Tailwind CSS를 적극 활용해 다크 모드를 구현할 것이다.
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;
위 코드를 예로 들면 name
에 EmailIcon
등 정의해 놓은 컴포넌트의 이름을 전달해주면 된다.
맘에 들지 않던 코드가 매우 깔끔해져서 기분이 좋다.
다크 모드 전역 상태 관리를 위해 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'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을 적용해 여러 언어를 토글할 수 있게 해보려고 한다.