📌 작성자 개발 환경
OS: Mac OS
Tool: Visual Studio Code

5 분 소요

til

Header

웹 페이지에서 공통 컴포넌트로 사용되는 Header 구현

Header style


styled-components를 사용하여 Header 컴포넌트의 스타일을 정의

//Header.style.ts
import { Link } from "react-router-dom";
import styled, { css } from "styled-components";
import { dropDown, dropUp, fadeIn, fadeOut } from "../../styles/keyframes";

export const LinkText = styled(Link)`
  display: flex;
  font-size: 1.3rem;
  font-family: "Yaldevi", sans-serif;
  font-weight: 700;
  color: #6d4924;
  transition: 0.3s;
  &:hover {
    transform: scale(110%, 110%);
    color: var(--purple);
    cursor: pointer;
  }
`;

export const SmallLinkText = styled.div`
  font-family: "Yaldevi", sans-serif;
  font-weight: 700;
  line-height: 15px;
  display: flex;
  height: 1.5rem;
  font-size: 14px;
  color: #6d4924;
  transition: 0.3s;
`;

export const Icon = styled(Link)`
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 80px;
  min-width: 60px;
  margin-top: 15px;

  &:hover > div {
    transform: scale(110%, 110%);
    color: var(--purple);
    cursor: pointer;
  }

  & > img {
    transition: 0.3s;
  }

  &:hover > img {
    transform: scale(110%, 110%);
  }
`;

export const IconDiv = styled.div`
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  width: 80px;
  min-width: 60px;
  margin-top: 15px;

  &:hover > div {
    transform: scale(110%, 110%);
    color: var(--purple);
    cursor: pointer;
  }

  & > img {
    transition: 0.3s;
  }

  &:hover > img {
    transform: scale(110%, 110%);
  }
`;

export const HeaderContainer =
  styled.div <
  { animation: string } >
  `
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  width: 100%;
  height: 80px;
  z-index: 1;
  align-items: center;
  justify-content: space-between;
  background-color: #fcfcffaa;
  animation: 0.3s ${({ animation }) =>
    animation === "fadeIn" ? fadeIn : fadeOut} forwards;
`;

export const AuthRelativeContainer = styled.div`
  display: flex;
  width: 18%;
  justify-content: center;

  @media screen and (max-width: 700px) {
    display: none;
  }
`;

export const DropDownContainer =
  styled.div <
  { isOpenMenu: boolean } >
  `
  display: none;
  position: relative;
  width: 10%;
  height: 100%;
  justify-content: center;
  align-items: center;

  @media screen and (max-width: 700px) {
    display: flex;
  }

  & > ul {
    ${({ isOpenMenu }) =>
      isOpenMenu
        ? css`
            visibility: visible;
            animation: ${dropDown} 0.3s forwards;
          `
        : css`
            animation: ${dropUp} 0.3s forwards;
          `}
  }
`;

export const DropDownContent = styled.ul`
  visibility: hidden;
  background-color: white;
  position: absolute;
  top: 100%;
  right: 0px;
  width: 150px;
  border-radius: 0px 0px 15px 15px;
  overflow: hidden;

  & > li,
  a > li {
    padding: 1rem;
    text-align: center;
    transition: 0.3s;
    display: flex;
    align-items: center;
    justify-content: space-around;

    &:hover {
      background-color: var(--normal-gray);
    }
  }
`;

export const HeaderLogo = styled.div`
  font-family: Just Another Hand, cursive;
  font-size: 2.5rem;
  cursor: pointer;
  display: flex;
  width: 18%;
  justify-content: flex-end;

  & > a {
    transition: 0.3s;
    color: #6d4924;
  }
  &:hover > a {
    transform: scale(110%, 110%);
    color: var(--purple);
  }
`;

export const HamburgerMenuStyle =
  styled.a <
  { isOpenModal: boolean } >
  `
  position: relative;
  width: 50px;
  height: 44px;
  cursor: pointer;

  & > span {
    width: 75%;
    height: 3px;
    left: 25%;
    background-color: #6d4924;
    position: absolute;
    border-radius: 4px;
    transition: all 0.3s;
  }

  & > span:nth-of-type(1) {
    top: 25%;
    ${({ isOpenModal }) =>
      isOpenModal ? "transform: translateY(11.5px) rotate(-45deg);" : ""}
  }

  & > span:nth-of-type(2) {
    top: 50%;
    ${({ isOpenModal }) => (isOpenModal ? "opacity: 0" : "")}
  }
  & > span:nth-of-type(3) {
    top: 75%;
    ${({ isOpenModal }) =>
      isOpenModal ? "transform: translateY(-11.5px) rotate(45deg);" : ""}
  }
`;

LinkText: styled-components를 사용하여 스타일이 적용된 Link 컴포넌트를 생성 이 컴포넌트는 링크를 표현하며, hover 시에 색상과 크기가 변경되는 애니메이션 효과가 적용된다.

Icon: 아이콘을 표현하기 위한 컴포넌트 아이콘과 관련된 이미지 및 스타일이 적용되며, hover 시에 크기와 색상이 변경되는 애니메이션 효과가 적용된다.

HeaderContainer: 헤더 컨테이너를 스타일링하는 컴포넌트입니다. 헤더의 크기, 배경색, 애니메이션 효과가 정의되어 있습니다.

position: fixed; //헤더 상단 고정

AuthRelativeContainer: 인증 관련 컨테이너를 스타일링하는 컴포넌트 미디어 쿼리를 사용하여 화면 크기에 따라 표시 여부가 조절된다.

DropDownContainer: 드롭다운 메뉴 컨테이너를 스타일링하는 컴포넌트 미디어 쿼리를 사용하여 화면 크기에 따라 표시 여부가 조절되며, 드롭다운 메뉴의 애니메이션 효과가 지정된다.

DropDownContent: 드롭다운 메뉴의 내용을 스타일링하는 컴포넌트 드롭다운 메뉴 아이템의 스타일이 정의되어 있다.

HamburgerMenuStyle: 햄버거 메뉴 아이콘을 스타일링하는 컴포넌트 햄버거 아이콘의 스타일 및 상태에 따른 애니메이션 효과가 정의되어 있다.

export const HamburgerMenuStyle = styled.a<{ isOpenModal: boolean }>`

isOpenModal이라는 프롭스를 받아와서 햄버거 메뉴를 오픈

  & > span:nth-of-type(1) {
    top: 25%;
    ${({ isOpenModal }) => (isOpenModal ? 'transform: translateY(11.5px) rotate(-45deg);' : '')}
  }

  & > span:nth-of-type(2) {
    top: 50%;
    ${({ isOpenModal }) => (isOpenModal ? 'opacity: 0' : '')}
  }
  & > span:nth-of-type(3) {
    top: 75%;
    ${({ isOpenModal }) => (isOpenModal ? 'transform: translateY(-11.5px) rotate(45deg);' : '')}
  }
`;

각각 첫 번째, 두 번째, 세 번째 span 요소에 대한 스타일을 지정

Header 컴포넌트


  • useLocation을 이용하여 현재 경로를 가져온다.
  • useNavigate를 이용하여 라우팅을 수행하는 함수를 가져온다.
  • useState를 이용하여 메뉴의 열림/닫힘 상태를 관리한다. useAuthAnimation 커스텀 훅을 사용하여 인증 관련 애니메이션 상태와 함수를 가져온다.
//Header.tsx
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import login_img from "../../assets/images/login_img.png";
import logout_img from "../../assets/images/logout_img.png";
import cart_img from "../../assets/images/cart_img.png";
import chatlist_img from "../../assets/images/chatlist_img.png";
import { LocalStorage } from "../../utils/browserStorage";
import {
  BASE_ANIMATION_TIME,
  LOCAL_STORAGE_KEY_LIST,
} from "../../assets/constantValue/constantValue";
import { postLogout } from "../../api/authApis";
import useScreenResize from "../../hooks/useScreenResize";
import useAuthAnimation from "../../hooks/useAuthAnimation";
import {
  AuthRelativeContainer,
  DropDownContainer,
  DropDownContent,
  HeaderContainer,
  HeaderLogo,
  Icon,
  IconDiv,
  LinkText,
  SmallLinkText,
} from "./Header.style";
import HamburgerMenu from "./HamburgerMenu";

function Header() {
  const location = useLocation();
  const navigate = useNavigate();
  const [isOpenMenu, setIsOpenMenu] = useState < boolean > false;
  const { animation, setAnimation } = useAuthAnimation();

  const handleResize = () => {
    if (window.innerWidth > 700) {
      setIsOpenMenu(false);
    }
  };

  const clickLogout = () => {
    postLogout()
      .then(() => {
        alert("로그아웃 되었습니다.");
        LocalStorage.clear();
        navigate("/");
      })
      .catch(() => console.log("로그아웃을 할 수 없습니다: noCookie"));
  };

  useScreenResize(handleResize);

  useEffect(() => {
    if (location.pathname === "/auth" && animation === "fadeIn") {
      setTimeout(() => setAnimation("fadeOut"));
      return;
    }
    if (location.pathname !== "/auth") {
      setTimeout(() => setAnimation("fadeIn"), BASE_ANIMATION_TIME);
      return;
    }
  }, [location]);

  return (
    <HeaderContainer animation={animation}>
      <HeaderLogo>
        <Link to="/">BUYTE</Link>
      </HeaderLogo>
      <div
        style=
      >
        <LinkText to="/map">Map</LinkText>
        <LinkText to="/select">Order</LinkText>
        <LinkText to="/mypage">Mypage</LinkText>
      </div>
      {LocalStorage.get(LOCAL_STORAGE_KEY_LIST.AccessToken) ? (
        <>
          <AuthRelativeContainer>
            {LocalStorage.get(LOCAL_STORAGE_KEY_LIST.MemberRole) ===
              "SELLER" && (
              <Icon to="/chatList">
                <img
                  src={chatlist_img}
                  alt="ChatList"
                  style=
                />
                <SmallLinkText>채팅목록</SmallLinkText>
              </Icon>
            )}
            <Icon to="/cart">
              <img src={cart_img} alt="Cart" style= />
              <SmallLinkText>장바구니</SmallLinkText>
            </Icon>
            <IconDiv onClick={clickLogout}>
              <img src={logout_img} alt="Login" style= />
              <SmallLinkText>LOGOUT</SmallLinkText>
            </IconDiv>
          </AuthRelativeContainer>
          <DropDownContainer
            onClick={() => setIsOpenMenu(!isOpenMenu)}
            isOpenMenu={isOpenMenu}
          >
            <HamburgerMenu isOpenMenu={isOpenMenu} />
            <DropDownContent>
              {LocalStorage.get(LOCAL_STORAGE_KEY_LIST.MemberRole) ===
                "SELLER" && (
                <Link to="/chatList">
                  <li>
                    <img
                      src={chatlist_img}
                      alt="ChatList"
                      style=
                    />
                    <div>채팅목록</div>
                  </li>
                </Link>
              )}

              <Link to="/cart">
                <li>
                  <img src={cart_img} alt="Cart" style= />
                  <div>장바구니</div>
                </li>
              </Link>
              <li onClick={clickLogout}>
                <img src={login_img} alt="Login" style= />
                <div>LOGOUT</div>
              </li>
            </DropDownContent>
          </DropDownContainer>
        </>
      ) : (
        <>
          <AuthRelativeContainer>
            <Icon to="/auth">
              <img src={login_img} alt="Login" style= />
              <SmallLinkText>LOGIN</SmallLinkText>
            </Icon>
          </AuthRelativeContainer>
          <DropDownContainer
            onClick={() => setIsOpenMenu(!isOpenMenu)}
            isOpenMenu={isOpenMenu}
          >
            <HamburgerMenu isOpenMenu={isOpenMenu} />
            <DropDownContent>
              <Link to="/auth">
                <li>
                  <img src={login_img} alt="Login" style= />
                  <div>LOGIN</div>
                </li>
              </Link>
            </DropDownContent>
          </DropDownContainer>
        </>
      )}
    </HeaderContainer>
  );
}
export default Header;

handleResize 함수

const handleResize = () => {
  if (window.innerWidth > 700) {
    setIsOpenMenu(false);
  }
};
  • innerWidth에 따라 앞에서 정의한 햄버거 메뉴를 오픈한다.

clickLogout 함수

const clickLogout = () => {
  postLogout()
    .then(() => {
      alert("로그아웃 되었습니다.");
      LocalStorage.clear();
      navigate("/");
    })
    .catch(() => console.log("로그아웃을 할 수 없습니다: noCookie"));
};
  • 로그아웃 버튼을 클릭했을 때 호출
  • 서버로 로그아웃 요청을 보내고, 성공적으로 로그아웃되면 LocalStorage를 초기화하고 홈 페이지로 이동한다.

useEffect 훅

useEffect(() => {
  if (location.pathname === "/auth" && animation === "fadeIn") {
    setTimeout(() => setAnimation("fadeOut"));
    return;
  }
  if (location.pathname !== "/auth") {
    setTimeout(() => setAnimation("fadeIn"), BASE_ANIMATION_TIME);
    return;
  }
}, [location]);
  • 현재 경로가 /auth이고 인증 애니메이션이 fadeIn 상태일 때, 로그인 페이지로 이동하는 경우에 애니메이션을 페이드아웃으로 변경
  • 그 외의 경우에는 /auth 경로가 아니면서 다른 경로로 이동할 때 애니메이션을 페이드인으로 변경

구현 이미지

Login X 처음 화면 Login O 화면 장바구니 페이지 접근 가능

댓글남기기