📌 다크모드 활용을 위해서 page내에 nav, body, footer에서도 Dark 모드를 쓴다는 정보를 알고 있어야 이를 공통적으로 사용할 수 있다.
useState를 이용해서 다크 모드를 쓰겠다는 걸 props로 전달을 해줘야 한다. page 내부에 아래 코드를 넣어준다.
const [isDark, setIsDark] = useState['Dark']
만약에, nav에서 다크 모드를 스위칭하는 요소가 있다고 하면, setIsDark의 참조도 같이 해줘야 한다. (보통 nav바에서 많이 구현한다.)
📌 nav 바 안에는 다양한 메뉴 카테고리가 있다. isDark라는 것을 이 요소들로 넘겨주기 위해서 props로 전달을 해준다. -> 깊이가 깊어질수록 계속 props로 넘기게 된다.
이렇게 계속 props를 다른 요소로 넘기게 되는 현상을 props drilling이라고 한다.
그렇기에 props가 너무 길어지다보면 관리가 많이 어려워지기 때문에 contextAPI 같은 전역 상태를 관리하여, 상위 컴포넌트에서 하위 컴포넌트로 손쉽게 데이터를 공유할 수 있게 한다.
const [isDark, setIsDark] = useState['Dark'] 이것이 page 안에 있는 것인데, 이 useState를 관리하는 중앙 저장소를 만든다고 생각하면 된다.
📌 useContext의 가장 중요한 개념은 우산을 씌워준다고 생각하면 된다. -> App 전체에서 한 상태에 대해서 바라볼 수 있는 우산을 씌워주고, 이 우산 안에 들어가 있는 요소들은 위에서 설명하는 Dark 모드라는 useState를 바로 꺼내서 쓸 수 있게 해준다. (props 없이.) 원래면, App -> page -> nav 이런 식으로 전부 props를 전달해야 했는데, 그냥 중앙 저장소(useContext)를 활용해서 바로 가져올 수 있다. nav보다 하위 요소들이 많아진다고 하더라도 context에서 한번에 받아올 수 있다.
📌예시) ContextPage를 만들고
import ContextPage from './06-useContext/ContextPage';
import './App.css';
export default function App() {
return (
<>
<ContextPage />
</>
);
}
import { useState } from "react";
import Navbar from "./Navbar";
import ThemeContent from "./ThemeContent";
export default function ContextPage() {
const [counter, setCounter] = useState(0);
return (
<>
<Navbar />
<ThemeContent counter={counter} />
</>
)
}
📃 tailwind css 버전 4에 따라서 index.css, App.tsx에 있는 기본 스타일은 전부 제거해주고, plugin만 해준다.
@import 'tailwindcss';
export default function ThemeContent(props) {
console.log(props);
return <div>ThemeContent {props.counter}</div>;
}
📃 이렇게 구현을 해주면 counter에 대해서 간단하게 뜨는 것을 볼 수 있다. (에러가 나는 코드긴 하다. 타입 때문에)
만약, 여기에 Menu.tsx가 있다고 할 때, 여기도 props로 전달을 해줘야 한다.
export default function Menu({ counter } : { counter: number }) {
return <div>{counter}</div>
}
import Menu from "./Menu";
export default function ThemeContent({counter} : {counter: number}) {
return (
<div>
ThemeContent <Menu counter={counter}/>
</div>
);
}
📃 이런 식으로 추가적으로 props를 넣어서 menu에서 사용되게 해야 한다. 이와 같이 계속 하위 요소가 쌓이면, 그에 맞춰서 props를 전달해줘야 하는데 이는 너무 복잡해질 수 있다. 그렇기 때문에, 전역 상태관리를 쓰는 것이 좋다. 앱이 전반적으로 동일한 상태를 유지해야 하는 경우(like 다크 모드, 로그인 유지 여부)에 쓰인다.
📃 여기서는 라이트 모드, 다크 모드를 적용을 ContextAPI로 연결을 하려고 한다. context를 만들 때는 폴더를 context 또는 provider, store 이런 식으로 만드는데 context로 진행. 테마와 관련된 것이기 때문에, ThemeProvider.tsx를 만들어준다.
일단, Context를 만들려면 만들겠다는 걸 선언해줘야 한다.
📃 상태를 만들어줄 거기 때문에, 뭔가 액션을 하는 형태가 있을 수도 있고, 직접 값을 관리하는 상태가 있을 수가 있어서 그에 대한 인터페이스랑 타입을 하나 만들어준다.
테마는 크게 2가지, 타입이나 enum으로 관리하면 되는데 enum으로 했다.
import { createContext } from "react";
enum THEME {
LIGHT = 'LIGHT',
DARK = 'DARK',
};
iinterface IThemeContextState { // 여기선 인터페이스 사용.
theme: THEME.LIGHT | THEME.DARK;
toggleTheme: () => void;
};
type TThemeContextAction = { // 여기서 이건 안 쓴다.
toggleTheme: () => void;
};
cconst ThemeContext = createContext<IThemeContextState | undefined>(undefined) // 값이 없을 수 있어서 undifined로.
📃 그 다음, 우산을 씌워주는 단계. 이 과정에서 value(인터페이스에 있는 상태들)를 전달을 해줘야 한다. 인터페이스에서 전달받을 때 좀 더 알아보기 쉽게
type TTheme = THEME.LIGHT | THEME.DARK;
으로 따로 변경을 해주고 선언을 해준다.
📃 ThemeProvider.tsx
import { createContext, PropsWithChildren, useContext, useState } from "react";
enum THEME {
LIGHT = 'LIGHT',
DARK = 'DARK',
};
type TTheme = THEME.LIGHT | THEME.DARK;
interface IThemeContextState {
theme: TTheme;
toggleTheme: () => void;
};
export const ThemeContext = createContext<IThemeContextState | undefined>(undefined) // 값이 없을 수 있어서 undifined로.
export const ThemeProvider = ({children}: PropsWithChildren) => {
const [theme, setTheme] = useState<TTheme>(THEME.LIGHT);
const toggleTheme = () => {
setTheme((prevTheme) =>
prevTheme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT
);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}> // key랑 value 값 같으면 생략 가능.
{children}
</ThemeContext.Provider>
)
}
// key랑 value 값 같으면 생략 가능.
export const useTheme = () => {
const context = useContext(ThemeContext);
if(!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}
📃 원래였으면, isDark를 props로 받아서 직접 할 텐데 위 contextAPI를 이용해서 구현할 수 있다.
📃 아래 코드에서 콘솔을 봤을 때 undefined가 뜨는데 그 이유는 ThemeProvider 즉, 우산을 씌워주질 않아서 발생하는 문제이다.
import { useContext } from "react";
import { ThemeContext } from "./context/ThemeProvider";
export default function Navbar() {
const context = useContext(ThemeContext);
console.log(context); // 새로고침을 해도 undefined로 뜨는 이유는 ThemeProvider 즉, 우산 안에 들어가 있지 않아서 그런 것이다.
return <div>Navbar</div>;
}
import Navbar from "./Navbar";
import ThemeContent from "./ThemeContent";
import { ThemeProvider } from "./context/ThemeProvider";
export default function ContextPage() {
return (
<ThemeProvider>
<Navbar />
<ThemeContent />
</ThemeProvider>
);
}
📃 navbar는 자신의 크기만 딱 받고, ThemeContent는 전체 페이지를 바뀌게 하고 싶다면,
1. main 요소가 될 거라고 생각되므로 <main></main> 태그를 적용한다.
2. className='flex-1'로 해주면, 자신의 크기에 속하는 것이 포함된다.
import Navbar from "./Navbar";
import ThemeContent from "./ThemeContent";
import { ThemeProvider } from "./context/ThemeProvider";
export default function ContextPage() {
return (
<ThemeProvider>
<div className='flex flex-col items-center justify-center min-h-screen'>
<Navbar />
<main className='flex-1'>
<ThemeContent />
</main>
</div>
</ThemeProvider>
);
}
📃 navbar에는 토글 즉, 버튼을 눌렀을 때, 라이트 모드인지 다크 모드인지 바꿀 수 있게 해줄 것이다. 그렇기 위해서 ThemeToggleButton.tsx를 만들고, Navbar에서 ThemeToggleButton.tsx를 import 해준다.
📢 참고: tailwind css를 쓰면 문제점이 있다. tailwind css는 동적 클래스를 감지 하지 못하기 때문에. (빌드 타입 문제.) 조건부 스타일을 주기 위해서는 clsx 라이브러리를 많이 사용한다. clsx를 사용했을 때, 어느정도 가독성이 좋아질 수 있다.
📌 ThemeToggleButton.tsx
tailwind css 적용을 할 때, 조건부 스타일을 주기 위해서 clsx 라이브러리를 다운로드 받는다.
https://www.npmjs.com/package/clsx
clsx
A tiny (239B) utility for constructing className strings conditionally.. Latest version: 2.1.1, last published: a year ago. Start using clsx in your project by running `npm i clsx`. There are 13313 other projects in the npm registry using clsx.
www.npmjs.com
$ npm install --save clsx
이 파일에서 라이트 모드인지, 다크 모드인지 여부도 알아야 한다.
import { THEME, useTheme } from "./context/ThemeProvider";
import clsx from "clsx";
export default function ThemeToggleButton() {
const { theme, toggleTheme } = useTheme(); // 라이트 모드인지, 다크 모드인지 여부도 알아야 한다.
const isLightMode = theme === THEME.LIGHT;
return (
<button onClick={toggleTheme}
className={clsx('px-4 py-2 mt-4 rounded-md transiton-all', {
'bg-black text-white': !isLightMode,
'bg-white text-black': isLightMode,
})}
>
{isLightMode ? '🌙 다크 모드' : '☀️ 라이트 모드'}
</button>
);
}
import clsx from "clsx";
import { THEME, useTheme } from "./context/ThemeProvider";
import ThemeToggleButton from "./ThemeToggleButton";
export default function Navbar() {
const {theme} = useTheme();
const isLightMode = theme === THEME.LIGHT;
return (
<nav className={clsx(
'p-4 w-full flex justify-end',
isLightMode ? 'bg-white' : 'bg-gray-800'
)}>
<ThemeToggleButton />
</nav>
);
}
import clsx from "clsx";
import { THEME, useTheme } from "./context/ThemeProvider";
export default function ThemeContent() {
const {theme} = useTheme();
const isLightMode = theme === THEME.LIGHT;
return (
<div
className={clsx(
'p-4 h-dvh w-full',
isLightMode ? 'bg-white' : 'bg-gray-800'
)}
>
<h1
className={clsx(
'text-wxl font-bold',
isLightMode ? 'text-black' : 'text-white'
)}
>
Theme Content
</h1>
<p className={clsx('mt-2', isLightMode ? 'text-black' : 'text-white')}>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempore assumenda
dolores expedita! Magni explicabo amet, vero qui nesciunt saepe dicta?
Quo cumque nisi, enim quaerat eum porro numquam repellendus reprehenderit.
</p>
</div>
);
}
이렇게 변경하면 화면이 전체적으로 적용이 될 것이다.
-> 이를 이용해서 TodoList에 그대로 적용해보면 바뀌는 것을 알 수 있다.
'UMC 8th Web 워크북' 카테고리의 다른 글
📜 useEffect 공식문서 간단 정리 (0) | 2025.04.05 |
---|---|
🫂왜 useEffect를 쓰는가 (Umc 워크북 3주차 강의.) (0) | 2025.04.04 |
🌐TodoList - ts + vite + yarn 세팅(tailwind css 적용.) (0) | 2025.03.29 |
🌐Tailwind Css 파헤쳐 보기💿 (0) | 2025.03.25 |
🌐리액트로 Todo List 만들기⛏️ (0) | 2025.03.24 |