[Next.js] 포트폴리오 웹 페이지 제작기 - 10. 반응형 웹 - 메인 화면, 슬라이드 메뉴

olwooz·2023년 2월 26일
0

페이지를 반응형으로 만들어 여러 크기의 화면에서 모두 자연스럽게 보일 수 있도록 해봐야겠다.
우선 메인 화면부터 구현할 예정이다.

메인 화면

width 640px 미만의 모바일 화면과 640px 이상의 넓은 화면 레이아웃을 다르게 구성해야 할 것 같다.
이는 두개의 div를 만들고 Tailwind CSS의 breakpoint를 사용해 쉽게 구현할 수 있다.

// components/Contents/Main/Main.tsx

import ContentWrapper from '../ContentWrapper';
import { textData } from './data';
import SlotMachine from './SlotMachine';
import { useTranslation } from 'next-i18next';

const Main = () => {
  const { t } = useTranslation('common');

  return (
    <ContentWrapper id="main" style="flex items-center">
      <div id="mobile" className="w-full sm:hidden">
        <h1 className="mb-6 text-base font-light">{t('main.greetings')}</h1>
        {textData.map((text) => (
          <h1 key="text" className="text-2xl font-thin">
            {t(`main.textData.${text}`)}
          </h1>
        ))}
        <h1 className="mt-6 text-base font-black">{t('main.introduction')}</h1>
      </div>

      <div className="hidden w-full sm:block">
        <h1 className="mb-6 text-2xl font-light">{t('main.greetings')}</h1>
        <SlotMachine textData={textData} />
        <h1 className="mt-4 text-4xl font-black">{t('main.introduction')}</h1>
      </div>
    </ContentWrapper>
  );
};

export default Main;

결과

모바일에서는 이렇게 나오게 된다.
상단 메뉴바와 아이콘바가 굉장히 애매하게 됐다.
메뉴바의 테두리는 이제 없애주고 shadow로 대체하겠다.
모바일에서는 메뉴와 아이콘을 슬라이드 메뉴로 옮겨주는게 나아 보인다.

슬라이드 메뉴

간단해 보이지만 은근 신경쓸 게 많다.
메뉴가 열려 있는지 여부를 판단할 상태, 그리고 그 상태를 이용해 스크롤 방지, 메뉴 외부 공간 클릭 시 메뉴 닫힘 처리 등 기능을 구현해줘야 한다.

상태

우선 슬라이드 메뉴를 위한 zustand 상태를 하나 만든다.

// hooks/useStore.ts

import { create } from 'zustand';

/* ... */

interface SlideMenuState {
  isOpen: boolean;
  toggleOpen: () => void;
}

/* ... */

const useStoreSlideMenu = create<SlideMenuState>((set) => ({
  isOpen: false,
  toggleOpen: () => set((state) => ({ isOpen: !state.isOpen })),
}));

export { useStoreDarkMode, useStoreSlideMenu };

구현

우선 구현을 했는데 버튼 관련 코드 리팩토링이 심각히 필요한 상황이다.
구현을 마치고 다음 글에서 곧바로 버튼 관련 코드를 리팩토링할 것이다.
간단하게 메뉴가 화면 우측에서부터 나타나는 모션을 줬다.
메뉴는 SlideMenuButton 안에서 onClick으로 토글시켜준다.

// components/SlideMenu/SlideMenu.tsx

import { useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useStoreSlideMenu } from '@/hooks/useStore';
import { RightArrowIcon } from '@/components/Icons';
import IconGroup from './IconGroup';
import MenuButton from './MenuButton';
import SlideMenuButton from './SlideMenuButton';

const SlideMenu = () => {
  const { isOpen, toggleOpen } = useStoreSlideMenu();

  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          key="slide-menu"
          className={`absolute top-0 right-0 z-20 ml-auto h-auto w-full translate-y-full transform bg-slate-200 text-center shadow-lg dark:bg-slate-800 dark:text-slate-200`}
          initial={{ x: '100%' }}
          animate={{ x: 0, transition: { duration: 0.4 } }}
          exit={{ x: '100%', transition: { duration: 0.4 } }}
        >
          <div className="absolute top-8 right-8">
            <SlideMenuButton>
              <RightArrowIcon />
            </SlideMenuButton>
          </div>
          <MenuButton name="main" />
          <MenuButton name="about" />
          <MenuButton name="projects" />
          <MenuButton name="contact" />
          <IconGroup />
        </motion.div>
      )}
    </AnimatePresence>
  );
};

export default SlideMenu;
// components/SlideMenu/SlideMenuButton.tsx

import { useStoreSlideMenu } from '@/hooks/useStore';

interface Props {
  children: React.ReactNode;
}

const SlideMenuButton = ({ children }: Props) => {
  const { toggleOpen } = useStoreSlideMenu();

  return <button onClick={toggleOpen}>{children}</button>;
};

export default SlideMenuButton;

바깥 공간 클릭 토글

useRef과 이벤트 리스너를 사용해 메뉴 바깥 공간을 클릭하면 메뉴가 닫히게 만들어준다.

// components/SlideMenu/SlideMenu.tsx

/* ... */
  const slideMenuRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    function handleClickOutside(event: MouseEvent) {
      if (slideMenuRef.current && !slideMenuRef.current.contains(event.target as Node)) {
        toggleOpen();
      }
    }

    document.addEventListener('mousedown', handleClickOutside);
    return () => {
      document.removeEventListener('mousedown', handleClickOutside);
    };
  }, [slideMenuRef, toggleOpen]);

  return (
    <AnimatePresence>
      {isOpen && (
        <motion.div
          key="slide-menu"
          className={`absolute top-0 right-0 z-20 ml-auto h-auto w-full translate-y-full transform bg-slate-200 text-center shadow-lg dark:bg-slate-800 dark:text-slate-200`}
          initial={{ x: '100%' }}
          animate={{ x: 0, transition: { duration: 0.4 } }}
          exit={{ x: '100%', transition: { duration: 0.4 } }}
          ref={slideMenuRef}
        >
/* ... */

스크롤 비활성화

메뉴가 열려 있을 때 스크롤을 비활성화하려면 bodyoverflowY: hidden을 주면 된다.

// pages/index.tsx

export default function Home() {
  const { isOpen } = useStoreSlideMenu();

  /* ... */
  
  useEffect(() => {
    document.body.style.overflowY = isOpen ? 'hidden' : 'visible';
  }, [isOpen]);

  /* ... */
}

결과

이런 형태의 슬라이드 메뉴가 만들어졌다.

0개의 댓글