본문 바로가기
웹 프로그래밍/🔮포트폴리오 사이트

[React] 포트폴리오 웹 사이트 제작 일지-11. 프로젝트 이미지 슬라이더 수정(캐러셀 슬라이드), 프로젝트 완성

by 청량리 물냉면 2024. 1. 9.
반응형

 

 

[React] 포트폴리오 웹 사이트 제작 일지-10. 프로젝트 슬라이더 만들기 및 반응형 웹으로 디자인

프로젝트 슬라이더 만들기 우선, project 컴포넌트 내부에 html로 작성해 두었던 프로젝트 내용들을 객체 배열 형태로 정리해서 projectItems라는 이름으로 따로 저장했다. export const projectItems = [ { title

florescene.tistory.com

이전 포스팅에서 제작한 프로젝트 슬라이더를 실제 휴대폰에서 확인해 보니 사용감이 좋지 않았다.

화면의 제약 때문에 잘리는 내용을 보여주기 위해 스크롤을 달았는데, 스크롤 과정에서 프로젝트를 모두 보여주지 못하고 다음 컴포넌트로 넘어가는 일이 잦아서 결국 프로젝트 슬라이더는 제거해 버렸다.

대신 프로젝트에 들어갈 이미지를 보여주는 데 슬라이더를 이용해 보기로 했다.

출처: https://css-tricks.com/css-only-carousel/

 

위와 같은 느낌의 슬라이더를 구현해 보았다.

 

 

하단 인덱스 dot 구현

1. 프로젝트별 이미지 인덱스, 현재 화면에 보이는 이미지 인덱스 저장

기존처럼 html 코드에 모든 정보를 직접 타이핑했었다면 훨씬 쉬웠을 것 같은데, 지난 번에 슬라이더를 만들면서 map을 이용해 모든 프로젝트를 동적으로 불러오고 있는 상황이었다.

dot를 적용하려면 각 프로젝트별 이미지의 전체 인덱스현재 화면에 보이는 이미지의 인덱스 번호를 모두 따로 저장해두어야만 했기 때문에 이 부분을 어떻게 한 번에 구현할 수 있을지 고민을 많이 했다.

 

고민하다가 가장 먼저 필요한 부분은 각 프로젝트별 인덱스를 저장할 객체를 만드는 일이라고 판단했다.

따라서 각 프로젝트별 이미지 인덱스 번호를 미리 저장해 두는 projectImage 배열을 생성했다.

const projectImage = [
  { project1: [0, 1, 2, 3, 4] },
  { project2: [0, 1, 2] },
  { project3: [0, 1, 2, 3, 4, 5] },
  { project4: [0, 1, 2, 3, 4] },
];

프로젝트1에는 총 5개의 사진이, 프로젝트2에는 총 3개의 사진이 들어간다. 필요한 인덱스만큼 미리 배열로 지정해 주었다.

그런 후에는 useState를 이용해 현재 각 프로젝트별로 몇 번째의 인덱스를 가리키고 있는지 저장할 imgIndexes를 생성했다. 

const [imgIndexes, setImgIndexes] = useState(
    projectImage.map((project) => 0)
  );

위 코드는 projectImage에 저장된 각 프로젝트의 현재 인덱스, 즉 화면에 보여지고 있는 이미지의 현재 인덱스를 모두 0으로 초기화시키는 코드이다. 

초기 imgIndexes = [0, 0, 0, 0]이다.

 

return (
    <div className="container project" ref={ref}>
     ...
        {projectItems.map((project, index) => {
          return (
            <Card className="project-card" key={index}>
              ...
              <div className="project-slider">
                <div
                  className="arrow left"
                  onClick={() => handlePrevProject(index)}
                ></div>
                <div className="img-container">
                  <img
                    src={
                      project.imageUrl[
                        projectImage[index][`project${index + 1}`][
                          imgIndexes[index]
                        ]
                      ]
                    }
                    alt={project.title}
                  />
                </div>
                <div
                  className="arrow right"
                  onClick={() => handleNextProject(index)}
                ></div>
              </div>
              ...
            </Card>
          );
        })}
      </div>
    </div>
  );
});

이미지 컨네이너 양 옆에 < > 버튼을 달았다.

각 버튼에는 onClick 이벤트로 함수가 적용되어 있으며, 함수 실행 시 현재 프로젝트 인덱스를 전달한다.

즉 project1일 시 0을, project2일 시 1을 전달한다.

이런 식으로 프로젝트마다 인덱스를 매개변수로 이용해 각각의 좌우 넘김 함수를 가질 수 있게 하였다.

중앙의 이미지 내부 코드가 조금 복잡한데 하나하나 풀어서 보자면

projectImage의 현재 프로젝트 인덱스 내부의 projectX번의 imageIndexes의 현재 인덱스의 이미지 url을 가져오는 코드이다. 

 

조금 더 이해하기 쉽게 예시를 들어보겠다.

0번째 프로젝트인 project1이 총 5개의 이미지 { project1: [0, 1, 2, 3, 4] } 를 가지고 있는 상황이라 가정해 보자.

현재 index = 0이며, `projectImage의 현재 프로젝트 인덱스(index=0) 내부의 projectX(0+1)번`은 아래 내용을 가리킨다.

 

const projectImage = [
  { project1: [0, 1, 2, 3, 4] },
  { project2: [0, 1, 2] },
  { project3: [0, 1, 2, 3, 4, 5] },
  { project4: [0, 1, 2, 3, 4] },
];

 

`projectX번의 imageIndexes의 현재 인덱스(index=0)`는 아래 내용을 가리킨다.

초기 imgIndexes = [0, 0, 0, 0]

 

따라서, 최종적으로는 project.imageUrl[0]을 가져오게 된다. 만약 현재 보이는 이미지의 인덱스가 변경되어 1, 2, 3이 된다면 이미지도 project.imageUrl[1], project.imageUrl[2], project.imageUrl[3]을 가져오게 될 것이다. 

project.imageUrl[xx]현재 인덱스(index=0)의 프로젝트의 이미지 url 배열 중 xx번째 사진을 가져오라는 뜻이다.

이런 식으로 이미지 인덱스를 가져오며 코드를 작성할 수 있었다.

 

2. 좌우 슬라이더 함수 구현

  const handlePrevProject = (index) => {
    setImgIndexes((prevIndexes) =>
      prevIndexes.map((prevIndex, i) => {
        if (i === index && prevIndex === 0) {
          return projectImage[i][`project${i + 1}`].length - 1;
        } else if (i === index) {
          return prevIndex - 1;
        }
        return prevIndex;
      })
    );
  };

 

좌우로 이미지를 바꾸어주는 함수도 이전 함수를 수정하여 구현했다.

위의 코드는 이미지 인덱스를 왼쪽으로 이동시키는 역할을 수행하는 함수이다.

 

prevIndexes 각 프로젝트의 현재 이미지 인덱스를 나타내는 배열(초기 imgIndexes = [0, 0, 0, 0])이며, 이 배열의 원소 전체를 순회하면서 현재 배열(prevIndexes) 원소의 인덱스 i매개변수로 전달받은 현재 프로젝트 인덱스인 index를 확인하며 작업을 수행한다.

  • prevIndexes.map((prevIndex, i) => { ... }): 각 프로젝트의 이미지 인덱스를 순회하면서 새로운 배열을 생성한다.
  • if (i === index && prevIndex === 0):
    • i === index: 프로젝트 배열의 인덱스와 현재 프로젝트 인덱스가 같은 경우. 이때의 preveIndex는 프로젝트의 현재 화면에 노출되는 이미지 인덱스를 의미한다.
    • 현재 선택된 프로젝트의 이미지가 첫 번째 이미지일 경우, 마지막 이미지의 인덱스로 변경한다.
  • else if (i === index): 그렇지 않다면 현재 선택된 프로젝트의 이미지 인덱스를 1 감소시킨다. (이전 이미지로 이동)
  • return prevIndex: i !== index의 경우. 두 인덱스가 일치하지 않는 경우는 각기 다른 프로젝트라는 의미다. 이 경우 해당 프로젝트에 대한 이미지 인덱스는 변경되지 않고 이전 값을 그대로 유지한다.

 

3.하단 인덱스 dot 구현

인덱스 dot도 각 프로젝트마다 다르게 달아주어야 한다.

활성화될 경우 다른 dot와 색상이 달라지도록 구현해야 했기 때문에, 현재 프로젝트의 이미지 인덱스의 값이 개별적으로 필요하다. 

 

먼저, 도트 이미지 클릭 시 원하는 인덱스 이미지로 변경시킬 수 있는 함수를 작성했다.

  const handleDotClick = (index, dotIndex) => {
    setImgIndexes((prevIndexes) =>
      prevIndexes.map((prevIndex, i) => (i === index ? dotIndex : prevIndex))
    );
  };

동일 프로젝트인 경우, 해당 프로젝트의 현재 화면에 보여지는 인덱스를 변경시킨다.

 

...

const Project = forwardRef((props, ref) => {
  ...
  return (
    <div className="container project" ref={ref}>
      <h1 className="main-title">Project</h1>
      <div className="project-card-wrapper">
        {projectItems.map((project, index) => {
          return (
            <Card className="project-card" key={index}>
              <h2 className="project-title">{project.title}</h2>
              ...
              <div className="dot-container">
                {projectImage[index][`project${index + 1}`].map(
                  (item, dotIndex) => (
                    <span
                      key={dotIndex}
                      className={`dot ${
                        dotIndex === imgIndexes[index] ? "active" : ""
                      }`}
                      onClick={() => handleDotClick(index, dotIndex)}
                    ></span>
                  )
                )}
              </div>
              ...
            </Card>
          );
        })}
      </div>
    </div>
  );
});

export default Project;

현재 프로젝트의 모든 이미지 인덱스를 순회한다.

예를 들어 프로젝트1이라면, { project1: [0, 1, 2, 3, 4] }의 [0, 1, 2, 3, 4]를 순회한다.

dotIndex와 imgIndexes[index] (index번째 프로젝트의 화면에 보이는 이미지의 인덱스)의 값이 동일하다면, 클래스명 active를 추가한다.

 

.dot-container {
  display: flex;
  justify-content: center;
  margin-top: 5px;
}

.dot {
  width: 10px;
  height: 10px;
  background-color: #ccc;
  border-radius: 50%;
  margin: 0 5px;
  cursor: pointer;
}

.dot.active {
  background-color: #555;
}

border-radius를 50%로 주어서 원을 그려주었다.

활성화된 dot는 다른 dot보다 진한 색으로 표시해 주었다.

 

 

CSS로 화살표 만들기

기존에 구현해 놓았던 슬라이더는 버튼에 이미지를 사용했는데, 마우스 버튼이 버튼 위에 올라가면 색상이 다른 색으로 바뀌며 활성화되었음을 보여주기 위해서, 화살표를 직접 구현하는 방식을 찾아보았다.

 

CSS로 화살표 < > 만들기

슬라이더를 사용하거나 버튼에 들어갈 < > 이런 화살표가 필요한 경우, 예전에는 이미지로 많이 넣었지만 요새는 디바이스 크기가 너무나도 다양해져서 해상도에 따라서 좀 깨져보이기도 한다.

hongpage.kr

위 블로그의 코드를 살짝 수정해서 화살표를 만들었다.

아래와 같이 양쪽 화살표 안에 프로젝트 이미지를 넣을 수 있도록 하였다.

...
              <div className="project-slider">
                <div
                  className="arrow left"
                  onClick={() => handlePrevProject(index)}
                ></div>
                <div className="img-container">
                  <img... />
                </div>
                <div
                  className="arrow right"
                  onClick={() => handleNextProject(index)}
                ></div>
              </div>
...

 

 

마우스 hover 시 화살표 색상 변경 👉 ::after

화살표 색상 변경이 안 되어서 애를 많이 먹었다.

기존 css 코드 중 after에 대한 지식이 부족해서 해결 방법을 고민하며 after에 대해 알아보았다.

before, aftercss 가상요소로, 실제 존재하지 않는 요소의 스타일링을 지정해준다. content 속성과 함께 특정 요소에 밑줄 효과 같은 장식용 컨텐트를 더해주는 데 사용된다.

이러한 배경 지식을 가지고 아래와 같이 코드를 수정해 보았다.

.arrow {
  position: relative;
  cursor: pointer;
}

.arrow.left:hover::after,
.arrow.right:hover::after {
  border-top: 2px solid #555;
  border-right: 2px solid #555;
}

.arrow.left::after,
.arrow.right::after {
  position: absolute;
  top: 50%;
  content: "";
  width: 15px;
  height: 15px;
  border-top: 2px solid #ccc;
  border-right: 2px solid #ccc;
}

.arrow.left::after {
  right: 50%;
  transform: rotate(225deg);
}

.arrow.right::after {
  left: 50%;
  transform: rotate(45deg);
}

내가 참고한 코드는 content 내용에 아무것도 지정해주지 않고, 대신 border를 이용해 각도를 조절하여 화살표를 만든 코드이다. 

hover 부분은 내가 추가한 내용으로, 사용자가 마우스를 hover 하면 화살표 색상을 바꾸어 준다.

처음에는 코드가 제대로 동작하지 않아서 애를 먹었는데 리액트 문제였는지 이후에는 정상적으로 동작했다. 시간을 많이 쏟았는데 허무한 에러였다... 그래도 이번 기회에 after에 대해 공부해 볼 수 있었다.

 

슬라이더 구현 결과


이미지 사이즈나 주변 컨테이너를 반응형으로 조절하는데 애를 많이 먹었다.

 

최종 코드

import { forwardRef, useState } from "react";
import Card from "../components/Card";
import "./Project.css";
import { projectItems } from "../components/ProjectItems";

const projectImage = [
  { project1: [0, 1, 2, 3, 4] },
  { project2: [0, 1, 2] },
  { project3: [0, 1, 2, 3, 4, 5] },
  { project4: [0, 1, 2, 3, 4] },
];

const Project = forwardRef((props, ref) => {
  const [imgIndexes, setImgIndexes] = useState(
    projectImage.map((project) => 0)
  );

  const handlePrevProject = (index) => {
    setImgIndexes((prevIndexes) =>
      prevIndexes.map((prevIndex, i) => {
        if (i === index && prevIndex === 0) {
          return projectImage[i][`project${i + 1}`].length - 1;
        } else if (i === index) {
          return prevIndex - 1;
        }
        return prevIndex;
      })
    );
  };

  const handleNextProject = (index) => {
    setImgIndexes((prevIndexes) =>
      prevIndexes.map((prevIndex, i) =>
        i === index
          ? prevIndex === projectImage[i][`project${i + 1}`].length - 1
            ? 0
            : prevIndex + 1
          : prevIndex
      )
    );
  };

  const handleDotClick = (index, dotIndex) => {
    setImgIndexes((prevIndexes) =>
      prevIndexes.map((prevIndex, i) => (i === index ? dotIndex : prevIndex))
    );
  };

  return (
    <div className="container project" ref={ref}>
      <h1 className="main-title">Project</h1>
      <div className="project-card-wrapper">
        {projectItems.map((project, index) => {
          return (
            <Card className="project-card" key={index}>
              <h2 className="project-title">{project.title}</h2>
              <h4>{project.category}</h4>
              <div className="project-slider">
                <div
                  className="arrow left"
                  onClick={() => handlePrevProject(index)}
                ></div>
                <div className="img-container">
                  <img
                    src={
                      project.imageUrl[
                        projectImage[index][`project${index + 1}`][
                          imgIndexes[index]
                        ]
                      ]
                    }
                    alt={project.title}
                  />
                </div>
                <div
                  className="arrow right"
                  onClick={() => handleNextProject(index)}
                ></div>
              </div>
              <div className="dot-container">
                {projectImage[index][`project${index + 1}`].map(
                  (item, dotIndex) => (
                    <span
                      key={dotIndex}
                      className={`dot ${
                        dotIndex === imgIndexes[index] ? "active" : ""
                      }`}
                      onClick={() => handleDotClick(index, dotIndex)}
                    ></span>
                  )
                )}
              </div>
              <section>
                <article className="info-article">
                  <p className="link-wrapper">Github</p>
                  <a href={project.githubLink}>{project.title}</a>
                  <p className="link-wrapper">배포 링크</p>
                  <a href={project.deployLink}>{project.deployLink}</a>
                  <p className="link-wrapper">개발 기록</p>
                  <a href={project.blogLink}>{project.blogLink}</a>
                </article>
                <article className="detail-article">
                  <h3>프로젝트 소개</h3>
                  <p>{project.description}</p>
                  <h3>주요 기능</h3>
                  <p>{project.features}</p>
                  <h3>기술스택</h3>
                  <p>{project.techStack}</p>
                  <h3>회고</h3>
                  <p>{project.retrospective}</p>
                </article>
              </section>
              <div className="project-order">
                {index + 1} / {projectItems.length}
              </div>
            </Card>
          );
        })}
      </div>
    </div>
  );
});

export default Project;

 

 

✅ 프로젝트 종료

 

배포 후 모바일 화면에서 확인하고 버그까지 수정하고 프로젝트가 종료되었다.

프로젝트 내부 컨텐츠는 프로젝트 진행할 때마다 수정하기로 했다.

포트폴리오 사이트는 여기서 종료!

 

 


후기

이력서 페이지를 만드는 데 대단한 기술이 들어가는 것도 아닌데 css 구현과 상태관리, 함수 작성만으로도 굉장히 힘이 들었다. css는 또 왜 이렇게 마음에 안 드는지 모르겠다...ㅠㅠ css만 따로 공부해보고 싶고, 그래서 성능 뿐 아니라 디자인적으로도 유저가 사용할 맛 나는 사이트를 만들겠다는 다짐을 하게 된 프로젝트였다.

개인적으로 처음 접해본 코드들이 많았고 충분히 많은 것을 배울 수 있는 기회기도 했다. 하나의 프로젝트를 완성했으니 이제 아직 미완성으로 남겨둔 프로젝트에 손을 대서 기술적인 역량을 더 키워나가고 싶다.

다음 프로젝트도 지치지 말고 배워가며 열심히 진행해 보기로 다짐하며 이번 프로젝트를 마무리하겠다.

반응형