[Next.js] 포트폴리오 웹 페이지 제작기 - 8. 다크 모드, 아이콘 리팩토링

olwooz·2023년 2월 18일
0

Tailwind CSS를 적극 활용해 다크 모드를 구현할 것이다.

'dark:' 클래스 추가

Tailwind CSS로 다크 모드를 구현하기가 아주 편한 이유는 바로 dark: 클래스를 이용해 다크 모드 스타일링을 쉽게 해줄 수 있다는 것이다.
기본적으로 prefers-color-scheme이라는 CSS media feature를 사용하기 때문에, 유저의 OS 등 환경 설정을 자동으로 따라가게 된다.

다크 모드 스타일이 컴포넌트 클래스들을 찾아 dark:text-slate-200, dark:bg-slate-800 등의 클래스를 추가해준다.

svg는 dark:fill-slate-300 과 같이 색을 바꿀 수 있다.

아이콘 리팩토링

다크 모드를 넣게 되면 아이콘의 색을 동적으로 변경할 수 있어야 하기 때문에 svg를 리액트 컴포넌트로 만든 아이콘으로 모두 변경했다.
이 과정에서 저번에 구현했었던 아이콘 코드의 중복을 줄이고 재사용성을 높일 수 있는 방법을 찾아내 적용했다.

우선 Icons 폴더에 index.ts를 만들어 여러 아이콘을 한 줄에 import할 수 있게 했다.

// components/Icons/index.ts

import { EmailIcon } from './EmailIcon';
import { GitHubIcon } from './GitHubIcon';
import { LightDarkIcon } from './LightDarkIcon';
import { PhoneIcon } from './PhoneIcon';
import { ShuffleIcon } from './ShuffleIcon';
import { TalkIcon } from './TalkIcon';
import { VelogIcon } from './VelogIcon';
import { IconOptions } from './types';

export { EmailIcon, GitHubIcon, LightDarkIcon, PhoneIcon, ShuffleIcon, TalkIcon, VelogIcon };

그리고 나서 타입을 정의하고 아이콘 모음 객체를 만들었다.

// components/Icons/types.ts

export type IconNames = 'EmailIcon' | 'GitHubIcon' | 'LightDarkIcon' | 
  'PhoneIcon' | 'ShuffleIcon' | 'TalkIcon' | 'VelogIcon';

export type IconOptions = {
  [K in IconNames]: React.ComponentType;
};
// components/Icons/index.ts

/* ... */

export const Icons: IconOptions = { EmailIcon, GitHubIcon, LightDarkIcon, PhoneIcon, ShuffleIcon, TalkIcon, VelogIcon };

이렇게 하면 아이콘을 사용하는 컴포넌트에서 변수 이름에 따라 다른 아이콘을 렌더할 수 있게 된다.

// componoents/IconBar/IconBar.tsx

import { Icons } from '@/components/Icons';
import { IconNames } from '@/components/Icons/types';

interface Props {
  name: IconNames;
}

const IconButton = ({ name }: Props) => {
  const Icon = Icons[name];

  return (
    <button className="mb-2 px-4">
      <Icon />
    </button>
  );
};

export default IconButton;

위 코드를 예로 들면 nameEmailIcon 등 정의해 놓은 컴포넌트의 이름을 전달해주면 된다.
맘에 들지 않던 코드가 매우 깔끔해져서 기분이 좋다.

토글 버튼

다크 모드 전역 상태 관리를 위해 zustand를 사용했다.

npm i zustand

우선 hook을 만들어주고

// hooks/useStore.ts

import { create } from 'zustand';

interface ToggleState {
  darkMode: boolean;
  toggleDarkMode: () => void;
}

const useStore = create<ToggleState>((set) => ({
  darkMode: true,
  toggleDarkMode: () => set((state) => ({ darkMode: !state.darkMode })),
}));

export default useStore;

tailwind.config.js에 클래스로 다크모드를 제어하겠다고 명시한다.

// tailwind.config.js

module.exports = {
  /* ... */
  darkMode: 'class',
};

그리고 index.tsx에 모든 컴포넌트를 감싸는 최상위 div를 만들어서 조건부로 dark 키워드를 준다.
조건은 아까 만든 zustand hook을 사용하면 된다.

// pages/index.tsx
import About from '@/components/Contents/About/About';
import Contact from '@/components/Contents/Contact/Contact';
import Main from '@/components/Contents/Main/Main';
import Projects from '@/components/Contents/Projects/Projects';
import Header from '@/components/Header/Header';
import IconBar from '@/components/IconBar/IconBar';
import { IconNames } from '@/components/Icons/types';
import Head from 'next/head';
import useStore from '@/hooks/useStore';

export default function Home() {
  const { darkMode } = useStore();

  const leftIcons: IconNames[] = ['LightDarkIcon'];
  const rightIcons: IconNames[] = ['VelogIcon', 'GitHubIcon'];

  return (
    <>
      <Head>
        <title>olwooz&apos;s portfolio</title>
        <meta name="description" content="olwooz's portfolio" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={darkMode ? 'dark' : ''}>
        <Header />
        <Main />
        <About />
        <Projects />
        <Contact />
        <IconBar direction="left" icons={leftIcons} />
        <IconBar direction="right" icons={rightIcons} />
      </div>
    </>
  );
}

마지막으로 토글 함수를 다크모드 버튼 아이콘 onClick에 넣어주면 된다.

// components/Icons/LightDarkIcon.tsx

import useStore from '@/hooks/useStore';

export const LightDarkIcon = () => {
  const { toggleDarkMode } = useStore();

  return (
    <svg
      height="24"
      viewBox="0 0 24 24"
      width="24"
      xmlns="http://www.w3.org/2000/svg"
      className="transition hover:scale-125 dark:fill-slate-300 dark:hover:fill-slate-100"
      onClick={toggleDarkMode}
    >
      <path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22ZM12 20.5V3.5C16.6944 3.5 20.5 7.30558 20.5 12C20.5 16.6944 16.6944 20.5 12 20.5Z" />
    </svg>
  );
};

여기서 사소한 디테일로 다크모드에 다른 스타일이 적용되는 컴포넌트 클래스에 transition을 추가하면 부드럽게 전환된다.

결과


다크모드도 잘 적용됐고, 아이콘 관련 코드도 나름 예쁘게 리팩토링이 된 것 같아 기분이 좋다.
최근에 타입스크립트를 더 공부한 덕에 타입을 의식하면서 개발하니까 코드의 질이 향상되는 느낌이다.

다음은 i18n을 적용해 여러 언어를 토글할 수 있게 해보려고 한다.

0개의 댓글