코딩/React

TailwindCSS 3.0과 Next.JS에서 Dark 모드 적용하기

드리프트 2022. 2. 25. 10:45
728x170

 

안녕하세요?

 

오늘은 TailwindCSS 3.0과 Next.JS에서 다크 모드(Dark Mode) 적용하는 방법에 대해 알아보겠습니다.

 

먼저, 빈 템플릿을 설치해 볼까요?

 

npx create-next-app tailwind-dark-mode

열심히 설치하고 있네요.

 

이제 TailwindCSS 최신판을 설치해 봅시다.

 

cd tailwind-dark-mode

npm i -D tailwindcss@latest postcss@latest autoprefixer@latest

 

TailwindCSS는 postcss와 autoprefixer 패키지가 필요합니다.

 

같이 설치해주셔야 합니다.

 

그다음에 TainwindCSS 초기화 명령을 실행시킵시다.

 

npx tailwindcss init -p

 

TailwindCSS 초기화 관련 파일들이 잘 생성되었습니다.

 

이제 tailwind.config.js 파일을 고쳐 보겠습니다.

 

TailwindCSS 3.0으로 넘어오면서 purge 항목이 사라지고 (JIT 컴파일러 자동 적용) content라는 항목을 씁니다.

 

이 content 항목에 아래와 같이 대상 js, jsx 파일을 적용시키면 됩니다.

 

우리는 NextJS라서 pages 폴더 밑에 있는 파일이 해당됩니다.

 

module.exports = {
  content: ["./pages/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

 

만약에 pages 폴더를 src 폴더 밑으로 둔다면 어떻게 될까요?

 

참고로 NextJS는 pages 폴더를 src 폴더 밑으로 옮겨도 알아서 판단하기 때문에 NextJS를 따로 건드릴 필요가 없습니다.

 

pages 폴더를 src 폴더로 옮기고 공용 컴포넌트 폴더인 components 폴더도 만들어 볼까요?

 

그리고 components 폴더 밑에 Layout.tsx 파일도 만들어 보겠습니다.

 

폴더 구조가 위와 같이 바뀌었습니다.

 

그러면 이제 tailwind.config.js 파일을 바꿔줘야 합니다.

 

module.exports = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx}",
    "./src/components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

그리고 Dark Mode를 적용할 거니까 아래 문구를 추가하겠습니다.

 

darkMode: 'class', // Tailwindcss 3.0 default is 'media',  'class'

darkMode를 class로 지정했습니다.

 

TailwindCSS 2.0 버전에서는 darkMode가 디폴트 값이 false인데요.

 

3.0으로 버전업 되면서 디폴트 값이 media 값으로 바뀌었습니다.

 

우리는 다크 모드를 만들 생각이니까 class로 지정하겠습니다.

 

아래 코드는 tailwind.config.js 최종 모습입니다.

 

module.exports = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx}",
    "./src/components/**/*.{js,ts,jsx,tsx}",
  ],
  darkMode: "class", // Tailwindcss 3.0 default is 'media',  'class'
  theme: {
    extend: {},
  },
  plugins: [],
};

 

그리고 한발 더 나아가 Typescript 도 설치해 보겠습니다.

 

먼저 tsconfig.json 파일을 빈 파일로 만들어 주십시오.

 

touch tsconfig.json

npm install -D typescript @types/react @types/node

 

그러고 나서 일반 개발서버를 한번 돌립니다.

 

npm run dev

 

Next.JS가 알아서 tsconfig.json 파일을 만들어 줬네요.

 

그리고 styles/globals.css 파일 좀 손 좀 봐야겠네요.

 

다시 개발 서버를 끕시다. (Ctrl + C)

 

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

 

위 파일이 바로 tsconfig.json 파일입니다.

 

여기서 좀 손 볼게 바로 src 폴더인데요.

 

다음과 같은 항목을 아무 데나 추가하시면 됩니다.

 

"baseUrl": "./src",

 

그리고 React에서 컴포넌트 Import 할 때 경로가 불편한 경우가 많기 때문에 다음과 같이 해주시면 편합니다.

 

"paths": {
      "~/*": [
        "./*"
      ]

 

위와 같이 넣어주면 import 하실 때 경로명을 "~/components/Layout" 이렇게 쉽게 쓸 수 있습니다.

 

전체적인 최종 tsconfig.json 파일입니다.

 

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "baseUrl": "./src",
    "paths": {
      "~/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

이제 Tailwind 모듈을 불러서 초기화해야겠죠?

 

일단 /src/pages/_app.js 파일을 _app.tsx 파일로 이름을 바꾸겠습니다.

 

그리고 나머지 index.js 파일도 index.tsx 파일로 이름을 바꾸시면 됩니다.

 

/src/pages/_app.tsx 파일 내용입니다.

import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

styles 폴더가 애매한데요.

 

이 폴더도 src 폴더 밑으로 옮기겠습니다.

 

그러면 다음과 같이 바꿀 수 있습니다.

 

import '~/styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

 

import ~ 형식인데요.

 

tsconfg.json 파일에서 우리가 paths 부분을 손본 게 이런 효과입니다.

 

tailwind.css 파일을 추가해볼까요?

 

import "tailwindcss/tailwind.css";
import '~/styles/globals.css'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

export default MyApp

 

우리가 설치한 tailwind 패키지에서 tailwind.css 파일을 import 하는 겁니다.

 

tailwind.css 파일에는 별거 없습니다.

 

아래와 같이 tailwindCSS 초기화 파일이 있을 뿐이죠.

 

@tailwind base;

@tailwind components;

@tailwind utilities;

 

사실 위 내용을 styles/globals.css 파일 맨 위에 적으셔도 상관없습니다.

 

이제 TailwindCSS 다크 모드를 테스트해볼까요?

 

먼저 Index.tsx 파일을 내용 다 지우고, 아래와 같이 간단하게 수정해 봅시다.

 

export default function Home() {
  return (
    <div>
      <h1 className="text-3xl text-pink-500">Welcome to Your App</h1>
    </div>
  );
}

이제 Tailwind 다크 모드를 강제로 적용해 볼까요?

 

먼저, /src/pages/_document.tsx 파일을 다음과 같이 만들어 봅시다.

 

import Document, { Html, Head, Main, NextScript } from "next/document";

class MyDocument extends Document {
  render() {
    return (
      <Html className="dark">
        <Head>
          <link href="/favicon.ico" rel="shortcut icon" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

위 파일에서 잘 보시면 Html 태그에 className이 "dark"로 지정되었습니다.

 

강제 dark 모드인 거죠.

 

이걸 "light"로 바꾸면 light 모드가 되는 겁니다.

 

index.tsx 파일에서 배경색을 추가해서 확인해 볼까요?

 

export default function Home() {
  return (
    <div className="h-screen w-full bg-gray-900">
      <h1 className="text-3xl text-pink-500">Welcome to Your App</h1>
    </div>
  );
}

다크 모드가 되었네요.

 

그럼 light 모드로 바꿔볼까요?

 

_document.tsx 파일에서 Html 태그의 className 값을 "light"로 바꾸면 됩니다.

 

<Html className="light">

 

결과를 볼까요?

 

 

안 바뀝니다. 왜 그런 걸까요?

 

TailwindCSS에서는 dark 모드는 다음과 같이 dark 모드일 때의 CSS를 따로 지정해 줘야 합니다.

 

bg-white dark:bg-gray-900    text-black  dark:text-white

 

위 코드를 보시면 bg-white는 light 모드일 때 적용하는 백그라운드 색깔이고요.

 

dark:bg-gray-900은 dark 모드일 때 적용하라는 백그라운드 색깔입니다.

 

그리고 뒤에 text-black dark:text-white는 텍스트 색깔입니다.

 

참고로 text-black은 디폴트이기 때문에 안 쓰셔도 됩니다. 예를 들어 적어 놓은 겁니다.

 

그럼 관련 CSS Class를 다시 적용해 볼까요?

 

index.tsx 파일입니다.

export default function Home() {
  return (
    <div className="h-screen w-full bg-white dark:bg-gray-900">
      <h1 className="text-3xl text-gray-900 dark:text-pink-500">
        Welcome to Your App
      </h1>
    </div>
  );
}

bg-white와 dark:bg-gray-900을 넣었습니다.

 

그리고 text도 text-gray-900과 dark:text-pink-500을 넣었습니다.

 

실행결과입니다.

 

아래는 _Document.tsx 파일에서 <Html className="light"> 일 경우고요.

아래는 _Document.tsx 파일에서 <Html className="dark"> 일 경우입니다.

 

완벽하게 적용되었네요.

 

즉, _Document.tsx 파일에서 <Html className="dark"> 이 부분의 className을 "light"와 "dark"로 바꾸면 라이트 모드에서 다크 모드로 왔다 갔다 할 수 있는 겁니다.

 

이제 TailwindCSS의 다크 모드 원리를 이해하셨나요?

 

그럼, 이걸 편하게 할 수 있는 패키지가 있으니까 그걸 적용해 보겠습니다.

 

우리가 직접 Dom을 찾아서 classList를 바꿔주는 코드는 TailwindCSS 홈페이지에 있으니 참고 바랍니다.

 

하지만 좀 더 편한 next-themes 이란 패키지를 사용해 볼까 합니다.

 

다음과 같이 설치해주시고요.

 

npm install next-themes

next-themes 패키지 적용 방법을 알아보겠습니다.

 

먼저, _app.tsx파일을 다음과 같이 수정합니다.

 

ThemeProvider를 attribute를 "class"로 지정해서 추가했습니다.

import "tailwindcss/tailwind.css";
import "~/styles/globals.css";
import { ThemeProvider } from "next-themes";

function MyApp({ Component, pageProps }) {
  return (
    <ThemeProvider attribute="class">
      <Component {...pageProps} />
    </ThemeProvider>
  );
}

export default MyApp;

 

이제 _Document.tsx 파일에서 Html 부분에 className을 지우셔도 됩니다.

 

import Document, { Html, Head, Main, NextScript } from "next/document";

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          <link href="/favicon.ico" rel="shortcut icon" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

 

이제 index.tsx 파일에서 다크 모드로 전환할 수 있는 버튼을 추가해 볼까 합니다.

 

import { useTheme } from "next-themes";

export default function Home() {
  const { theme, setTheme } = useTheme();

  return (
    <div className="h-screen w-full bg-white dark:bg-gray-900">
      <h1 className="text-3xl text-gray-900 dark:text-pink-500">
        Welcome to Your App
      </h1>

      <button
        type="button"
        onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
        className="text-gray-500 dark:text-gray-400"
      >
        {theme === "light" ? (
          <svg
            className="w-5 h-5"
            fill="currentColor"
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path>
          </svg>
        ) : (
          <svg
            className="w-5 h-5"
            fill="currentColor"
            viewBox="0 0 20 20"
            xmlns="http://www.w3.org/2000/svg"
          >
            <path
              d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
              fillRule="evenodd"
              clipRule="evenodd"
            ></path>
          </svg>
        )}
      </button>
    </div>
  );
}

 

next-themes 패키지 사용방법은 간단합니다.

 

useTheme 훅을 불러와서 theme와 setTheme를 적절하게 사용하면 됩니다.

 

그래서 button onClick에서 다음처럼 setTheme을 토글 방식으로 적용한 겁니다.

<button
        type="button"
        onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
        className="text-gray-500 dark:text-gray-400"
      >

 

이렇게 하면 next-themes 패키지가 자동으로 _Documents.tsx 파일의 Html 태그에서 className을 "light"와 "dark"로 바꿔주게 되는 거죠.

 

한번 테스트 결과를 볼까요?

 

 

버튼 모양은 SVG 형식을 썼는데 자주 쓰시는 아이콘 같은 거 쓰시면 됩니다.

 

그럼.

그리드형