포트폴리오 사이트를 만들기 시작했다
지금까지는 기획상 애니메이션 등 보여지는 요소들을 많이 구현해보지 못했는데 포트폴리오는 보여지는게 목적이다보니 궁금했던 모션이나 상호작용 등을 활용해보려한다
첫번째로 다크모드를 처음 시도해보려한다
next-themes
Next.js에서 다크모드를 비롯한 테마 변경을 쉽게 구현할 수 있도록 하는 라이브러리
클라이언트 측에서 localStorage 또는 class 속성을 사용해 테마를 적용하고 유지하도록 한다
주요 기능 및 이점
- useTheme() 훅을 사용해 테마를 간편하게 변경할 수 있음
- 사용자가 선택한 테마를 localStorage로 유지
- class 기반 테마를 지원해 tailwindCSS를 활용할 때 유용
- system 옵션을 사용해 사용자의 OS 테마 설정에 따라 자동으로 테마가 적용되도록 할 수 있음
사용법
1) next-themes 설치
pnpm install next-themes
2) ThemeProvider 설정
최상위 레이아웃 파일에 ThemeProvider를 추가해 테마 변경이 모든 페이지에 적용되도록 한다
나는 Next.js를 사용하고 있으므로 최상위 layout.tsx에 적용!
더 마음에 드는 디자인이 다크모드여서 다크모드를 기본테마로 설정했다 (defaultTheme='dark')
그리고 tailwindCSS와 호환을 위해 attribute='class'로 설정했다
"use client";
import { ThemeProvider } from "next-themes";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<ThemeProvider attribute="class" defaultTheme="dark">
{children}
</ThemeProvider>
</body>
</html>
);
}
3) tailwind.config.ts 설정
tailwind에서 다크모드를 class 기반으로 적용하도록 설정한다
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: [
"./app/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
};
4) 테마 토글 버튼 구현
임시로 사용할 토글을 만들었다
'use client';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export default function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => {
setTheme(theme === 'dark' ? 'light' : 'dark');
}}
>
{theme === 'dark' ? '라이트모드' : '다크모드'}
</button>
);
}
Hydration 에러 해결하기
테스트를 위해 야심차게 실행시켜봤지만.. 바로 에러가 떴다
서버에서는 버튼이 '다크모드'인데 클라이언트에서는 '라이트모드'여서 두 상태가 일치하지 않는다는 것이다

Error: Text content does not match server-rendered HTML. See more info here: https://nextjs.org/docs/messages/react-hydration-error
Text content did not match. Server: "다크모드" Client: "라이트모드"
useEffect 활용
클라이언트에서 마운트된 후 현재 적용된 theme을 확인해 값을 설정하도록 했다
// ThemeToggle.tsx
'use client';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
export default function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// 서버 렌더링 중에는 기본 테마 사용
const currentTheme = mounted ? resolvedTheme : 'dark';
return (
<button onClick={() => setTheme(currentTheme === 'dark' ? 'light' : 'dark')}>
{currentTheme === 'dark' ? '라이트모드' : '다크모드'}
</button>
);
}
Hydration Error는 해결됐지만, 이렇게 구현하는 경우 기본값이 다크모드이기 때문에 라이트모드로 설정한 사용자도 새로고침시 '라이트모드'라는 버튼이 렌더링 됐다가 다시 '다크모드' 버튼으로 변하는 깜빡임이 발생한다

깜빡임 문제 해결 - skeleton ui 활용
이 문제를 해결하려면 서버에서부터 사용자의 localStorage에 저장된 값을 알 수 있어야한다..
html에 적용된 data-theme을 가져오는 것도, localStorage에 저장된 theme을 가져오는 것도 모두 마운트된 후에 가능하기 때문에 사용자 입장에선 이게 로딩이 되지 않은 상태라고 생각이 들었고 skeleton ui를 적용했다
'use client';
import { useTheme } from 'next-themes';
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const toggleTheme = (): void => {
if (!theme) return;
const newTheme = theme === 'dark' ? 'light' : 'dark';
setTheme(newTheme);
};
if (!mounted) {
return <button className='w-[120px] h-[40px] rounded-md animate-pulse bg-gray-300'>{/* 로딩 중 버튼 */}</button>;
}
return (
<button
onClick={toggleTheme}
className={`w-[120px] h-[40px] flex items-center justify-center rounded-md transition ${
theme === 'dark' ? 'bg-gray-800 text-white hover:bg-gray-700' : 'bg-gray-200 text-black hover:bg-gray-300'
}`}
>
{theme === 'dark' ? '라이트모드' : '다크모드'}
</button>
);
}

거슬리던 현상은 모두 개선이 됐다
그런데 굳이 skeleton ui를 활용하지 않아도 가능한 방법이 있을 것 같아서..
내일 한번 더 알아봐야겠다!!
'개인 프로젝트 > 포트폴리오' 카테고리의 다른 글
Next.js로 포트폴리오 만들기 2 - 다크모드 개선기 (Extra attributes from the server: data-theme,style 에러 해결, 서버에서 클라이언트 정보를 아는 법) (0) | 2025.03.10 |
---|