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

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

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

프로젝트 슬라이더 만들기

 

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

export const projectItems = [
  {
    title: "🌓Dream note",
    category: "개인 프로젝트",
    imageUrl: ``,
    githubLink: "",
    deployLink: "",
    description:
      "",
    features:
      "",
    retrospective:
      "",
    techStack: "",
  },
  {
    title: "⏰Stop Watch",
    category: "개인 프로젝트",
    imageUrl: ``,
    githubLink: "",
    deployLink: "",
    description:
      "",
    features:
      "",
    retrospective:
      "",
    techStack: "",
  },
];

 

이후 import를 이용해서 불러와 사용했다.

 

import { projectItems } from "./ProjectItems";

...

const [currentProjectIndex, setCurrentProjectIndex] = useState(0);

  const handlePrevProject = () => {
    setCurrentProjectIndex((prevIndex) =>
      prevIndex === 0 ? projectItems.length - 1 : prevIndex - 1
    );
  };

  const handleNextProject = () => {
    setCurrentProjectIndex((prevIndex) =>
      prevIndex === projectItems.length - 1 ? 0 : prevIndex + 1
    );
  };

< 프로젝트 > (프로젝트 양 옆 화살표) 와 같은 식으로, < > 버튼을 누르면 프로젝트가 변경되는 슬라이드를 구현할 예정이기 때문에, 현재 화면에 보여줄 프로젝트 인덱스를 저장할 객체 currentProjectIndex를 생성했다. 

 

handlePrevProject() 함수와 handleNextProject() 함수는 버튼을 누르면 화면에 보여줄 프로젝트의 인덱스를 바꾸어주는 함수이다.

함수에 대해 자세히 설명해 보자면, 두 함수 모두 setter 함수 setCurrentProjectIndex의 인자로 새로운 상태를 반환하는 콜백 함수를 전달해주고 있다. 이 콜백 함수의 매개변수인 (prevIndex)는 현재의 프로젝트 인덱스를 나타낸다.

handlePrevProject() 함수의 경우 이 값이 0이면 마지막 프로젝트로 이동하고, 그렇지 않으면 이전 프로젝트로 이동하게 된다.

반대로 handleNextProject() 함수의 경우 prevIndex 값이 마지막 프로젝트의 인덱스를 가리키고 있다면 0번째 프로젝트로 이동하고, 그렇지 않다면 다음 프로젝트로 이동한다.

 

  return (
    <div className="container project" ref={ref}>
      <h1 className="main-title">Project</h1>
      <div className="project-card-wrapper">
        <div className="slide">
          <img
            src={process.env.PUBLIC_URL + "/assets/images/left01.png"}
            onClick={handlePrevProject}
          />
        </div>
        <Card className="project-card">
          <div className="project-order">
            {currentProjectIndex + 1} / {projectItems.length}
          </div>
          <h2 className="project-title">
            {projectItems[currentProjectIndex].title}
          </h2>
          <h4>{projectItems[currentProjectIndex].category}</h4>
          <section>
            <article className="info-article">
              <img
                src={projectItems[currentProjectIndex].imageUrl}
                alt={projectItems[currentProjectIndex].title}
              />
              <p className="link-wrapper">Github</p>
              <a href={projectItems[currentProjectIndex].githubLink}>
                {projectItems[currentProjectIndex].title}
              </a>
              <p className="link-wrapper">배포 링크</p>
              <a href={projectItems[currentProjectIndex].deployLink}>
                {projectItems[currentProjectIndex].deployLink}
              </a>
              <p className="link-wrapper">개발 기록</p>
              <a href={projectItems[currentProjectIndex].blogLink}>
                {projectItems[currentProjectIndex].blogLink}
              </a>
            </article>
            <article>
              <h3>프로젝트 소개</h3>
              <p>{projectItems[currentProjectIndex].description}</p>
              <h3>주요 기능</h3>
              <p>{projectItems[currentProjectIndex].features}</p>
              <h3>회고</h3>
              <p>{projectItems[currentProjectIndex].retrospective}</p>
              <h3>기술스택</h3>
              <p>{projectItems[currentProjectIndex].techStack}</p>
            </article>
          </section>
        </Card>
        <div className="slide">
          <img
            src={process.env.PUBLIC_URL + "/assets/images/right01.png"}
            onClick={handleNextProject}
          />
        </div>
      </div>
    </div>
  );

전체 코드를 살펴보면, projectItems 데이터를 이용해 동적으로 데이터를 화면에 뿌려주고 있다.

위에서 생성한 함수는 프로젝트 카드인 <Card className="project-card"></Card> 양 옆의 <div className="slide"></div> 내부 화살표 이미지에 각각 달아주었다.

함수가 연결된 이미지를 클릭하면 카드 내부의 정보가 동적으로 바뀐다.

 

프로젝트 슬라이더 결과 화면

 

 

반응형 웹으로 디자인 수정

헤더

반응형으로 수정을 하면서 가장 달라지는 컴포넌트는 헤더이다.

화면의 사이즈가 작아지면 헤더 메뉴바의 글자들이 잘리기 때문에, 일반적으로 다른 웹 페이지에서 하듯이 토글 버튼(☰)을 누르면 메뉴 리스트가 아래에 나타나도록 구현했다.

 

코드

  const [toggleMenu, setToggleMenu] = useState(false);

  const handleToggleMenu = (prev) => {
    setToggleMenu((prev) => !toggleMenu);
  };

가장 먼저 토글메뉴(화면 사이즈 작아졌을 시)의 현재 상태를 저장할 toggleMenu를 생성하고 초기값을 false로 지정했다.

handleToggleMenu() 함수는 현재 toggleMenu값의 False 값으로 toggleMenu를 업데이트해주는 함수이다.

이제 handleToggleMenu()를 이용해 토글바 ↔ 토글메뉴 전환 코드를 작성해 보겠다.

 

      <div className="header-contents">
        <div className="title-toggle">
          <span className="header-title" onClick={moveToHome}>
            Portfolio
          </span>
          {/* 토글 버튼 */}
          <div className="toggle-button">
            <img
              src={
                process.env.PUBLIC_URL +
                `/assets/images/${toggleMenu ? `cancle` : `menu`}.png`
              }
              onClick={handleToggleMenu}
            />
          </div>
        </div>
        {/* 메뉴 리스트 */}
        <ul className={`menu-list ${toggleMenu ? "visible" : ""}`}>
          <li
            className={`menu-item ${selectedPage === "archiving" && "visible"}`}
          >
            <div onClick={moveToArc}>ARCHIVING</div>
          </li>
          ...
        </ul>
      </div>
     ...

토글 버튼을 프로젝트 타이틀명 span태그 하단에 추가했다.

두 항목을 나란히 나열하기 위해 아래와 같이 css를 작업 했다.

.header-contents {
  display: flex;
  padding: 20px 0;
  margin: 0 auto;
  flex-direction: row;
  justify-content: space-between;
}

flex-direction을 row로 하여 가로로 두 항목을 정렬했다.

 

메뉴 리스트 부분은 이전과 동일하게 유지했다.

대신 toggleMenu의 값에 따라 클래스명 "visible"을 추가하여 css 스타일링을 통해 항목을 숨기거나 노출시켰다.

...

.menu-item.visible {
  font-weight: 600;
  background-color: #f2dd68;
  color: #161616;
}

@media screen and (min-width: 280px) and (max-width: 750px) {
  .menu-list {
    display: none; /* 기본적으로 메뉴 숨김 */
    flex-direction: column;
    padding: 10px 0;
    align-items: center;
  }
  
  .menu-list.visible {
    display: flex; /* 토글 버튼 클릭 시 메뉴 보이도록 변경 */
    flex-direction: column;
  }
}

미디어 쿼리를 이용해 원하는 화면 크기에서 보여줄 스타일링을 작성했다.

className에 visible이 붙어있을 때는 display: none을 이용해 메뉴를 숨겨두었다.

이때 사용자의 요청에 따라 토글 버튼이 클릭되고 toggleMenu의 값이 true가 되면 .menu-list.visible이 활성화된다.

이때 display: flex로 스타일링이 변경되며 메뉴가 화면에 보이게 된다. 이때 보이는 메뉴 항목들은 세로로 정렬시켰다.

 

결과 화면

기본
토글 버튼 클릭 시

 

스크롤 시 메뉴가 열려있으면 닫기

추가로 메뉴화면이 열린 상태로 스크롤이 진행될 시 메뉴가 계속 열린 상태인 것이 거치적거려서, 자동으로 닫아주는 기능을 구현했다.

사용자의 스크롤을 인식하면 ToggleMenu값이 false가 되어 메뉴리스트가 닫히는 원리이다.

  const [toggleMenu, setToggleMenu] = useState(false);

  const handleToggleMenu = () => {
    setToggleMenu(!toggleMenu);
  };

  useEffect(() => {
    // "scroll" 이벤트 발생 시 실행되는 함수
    const handleScroll = () => {
      if (toggleMenu) {
        // 토글바가 열려있을 때 스크롤이 발생하면 토글바를 닫음
        setToggleMenu(false);
      }
    };
	
    // 스크롤 이벤트 리스너 등록
    //  "scroll" 이벤트 발생 시 handleScroll 함수 실행
    window.addEventListener("scroll", handleScroll);

    return () => {
      // 컴포넌트 언마운트 시 이벤트 리스너 제거
      window.removeEventListener("scroll", handleScroll);
    };
  }, [toggleMenu]);

코드 구현시 useEffect를 사용한 이유는 컴포넌트의 렌더링과는 독립적으로 사용자의 스크롤을 감지했을 때만 내부 코드를 실행하게 하기 위해서이다. 

useEffect 내부에서 스크롤 이벤트 발생 시 handleScroll 함수를 실행하도록 이벤트 리스너를 등록해 주었다.

handleScroll 함수는 현재 토글바가 열려 있다면 토글바를 닫는 역할을 수행한다. 

메모리 누수를 방지하기 위해 언마운트 시 이벤트 리스너를 제거하는 코드도 작성했다.

 

반응형 헤더 전체 코드

Header.js

import { useEffect, useState } from "react";
import "./Header.css";

const Header = ({
  moveToHome,
  moveToArc,
  moveToSkill,
  moveToPrj,
  moveToEdu,
  moveToContact,
  selectedPage,
}) => {
  const [toggleMenu, setToggleMenu] = useState(false);

  const handleToggleMenu = () => {
    setToggleMenu(!toggleMenu);
  };

  useEffect(() => {
    const handleScroll = () => {
      if (toggleMenu) {
        setToggleMenu(false);
      }
    };
    window.addEventListener("scroll", handleScroll);

    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [toggleMenu]);

  return (
    <header className="header">
      <div className="header-contents">
        <div className="title-toggle">
          <span className="header-title" onClick={moveToHome}>
            Portfolio
          </span>
          {/* 토글 버튼 */}
          <div className="toggle-button">
            <img
              src={
                process.env.PUBLIC_URL +
                `/assets/images/${toggleMenu ? `cancle` : `menu`}.png`
              }
              onClick={handleToggleMenu}
            />
          </div>
        </div>
        {/* 메뉴 리스트 */}
        <ul className={`menu-list ${toggleMenu ? "visible" : ""}`}>
          <li
            className={`menu-item ${selectedPage === "archiving" && "visible"}`}
          >
            <div onClick={moveToArc}>ARCHIVING</div>
          </li>
          <li className={`menu-item ${selectedPage === "skills" && "visible"}`}>
            <div onClick={moveToSkill}>SKILLS</div>
          </li>
          <li
            className={`menu-item ${selectedPage === "project" && "visible"}`}
          >
            <div onClick={moveToPrj}>PROJECT</div>
          </li>
          <li className={`menu-item ${selectedPage === "edu" && "visible"}`}>
            <div onClick={moveToEdu}>QUALIFICATION</div>
          </li>
          <li
            className={`menu-item ${selectedPage === "contact" && "visible"}`}
          >
            <div onClick={moveToContact}>CONTACT</div>
          </li>
        </ul>
      </div>
    </header>
  );
};

export default Header;

 

Header.css

.header {
  position: fixed;
  top: 0;
  right: 0;
  left: 0;
  background-color: #1616168c;
  z-index: 1;
  backdrop-filter: blur(10px);
}

.header-contents {
  width: 100%;
  display: flex;
  padding: 20px 0;
  margin: 0 auto;
  flex-direction: row;
  justify-content: space-between;
}

.title-toggle {
  position: relative;
  padding: 0 40px;
  text-align: center;
}

.header-title {
  margin: 0;
  font-size: 22px;
  cursor: pointer;
  font-family: "Montserrat", sans-serif;
}

.toggle-button {
  display: none;
}

.toggle-button > img {
  position: absolute;
  top: 40%; /* 이미지를 세로 중앙에 정렬 */
  right: 20px; /* 이미지를 헤더 타이틀 오른쪽에 위치 */
  transform: translateY(-50%);
  width: 26px;
  height: 26px;
  cursor: pointer;
}

.menu-list {
  list-style: none;
  display: flex;
  flex-direction: row;
  padding: 0 35px;
  margin: 0px;
  justify-content: space-around;
}

.menu-item {
  text-align: center;
  padding: 2.3px 10px 0 10px;
  margin: 0 5px;
  font-family: "Poppins", sans-serif;
  font-size: 15px;
  cursor: pointer;
}

.menu-item.visible {
  font-weight: 600;
  background-color: #f2dd68;
  color: #161616;
}

@media screen and (min-width: 280px) and (max-width: 750px) {
  .header-contents {
    display: flex;
    flex-direction: column;
  }

  .title-toggle {
    padding: 0;
    text-align: center;
  }

  .header-title {
    font-size: 25px;
  }

  .menu-list {
    display: none; /* 기본적으로 메뉴 숨김 */
    flex-direction: column;
    padding: 10px 0;
    align-items: center;
  }

  .menu-item {
    line-height: 25px;
    margin-top: 10px;
  }

  .toggle-button {
    display: flex;
    margin-right: 40px;
    cursor: pointer;
  }

  .menu-list.visible {
    display: flex; /* 토글 버튼 클릭 시 메뉴 보이도록 변경 */
    flex-direction: column;
  }
}

 

반응형 헤더 결과 화면

 

기타 다른 컴포넌트까지 반응형 디자인 적용한 뒤 결과 화면

전체 화면
화면 줄였을 때. 추가로 구현한 사항이 있어 위의 전체 화면과 구성이 일부 다르다.

 

더보기

소감

JS 코드 작성하는 데도 시간이 많이 걸렸지만, 스타일링에 더 오랜 시간을 쏟았다.

반응형 헤더 작성하는 데도 토글버튼 위치를 지정하는 데 오래 걸렸다. css 공부에 지금보다 더 시간을 쏟아야겠다...

 

반응형