일반적으로 React에서는 데이터는 부모로부터 props를 통해 전달된다. 그러나 컴포넌트를 나누다보면 하나의 어플리케이션 안에 여러 컴포넌트에 전달해줘야 하는 경우 props-drilling 현상이 나타나기도 한다. props로 계속해서 자식 컴포넌트에 전달하면 코드가 지저분해진다.
리덕스는 자바스크립트 라이브러리로 상태 컨테이너의 역할을 하며 리액트에서는 전역 상태 관리 도구로 많이 활용된다. 리액트 context는 상태 관리 훅과 조합하여 사용되었을 때 상태 관리 도구로 활용할 수 있으나 Context와 Redux는 다른 도구이며 다른 목적을 가진다.
도구를 사용할 때에는 자신의 상황을 먼저 생각한 뒤 그에 맞게 써야한다.
- Redux를 사용해야 할 때
- Context API를 사용해야 할 때
그외에도 recoil, mobX가 있다.
리액트에서 제공되는 createContext를 활용해 어플리케이션에 사용될 Context 객체를 만든다. 어플리케이션에서 테마(다크모드, 라이트모드)와 사용자 정보를 Context로 생성해보면
context.js
import { createContext } from "react";
export const ThemeContext = createContext("dark");
export const UserContext = createContext({ name: "Guest" })
createContext 함수에 들어가는 매개변수는 적절한 Provider를 찾지 못했을 때만 쓰이는 값으로 이 기본값은 컴포넌트를 독립적으로 테스트할 때 유용하다. 즉 Provider에서 값을 제공한다면 굳이 인자를 주지 않아도 된다. 그럼 Provider를 생성해보면
contextName.Provider
라는 컴포넌트는 모든 자식 컴포넌트에게(얼마나 깊은 컴포넌트인가와 무관하게) ContextName
에 해당하는 context를 provider해준다. 이 때, 반드시 value
라는 이름의 prop으로만 context를 넘겨줄 수 있다.
import React from "react";
import { ThemeContext, UserContext } from './context'
export default function Main() {
return (
<ThemeContext.Provider value="light">
<UserContext.Provider value={{ name: "Lee" }}>
<MyComponent />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
Provider에서 초깃값을 제공했기 때문에 createContext에 인자를 주지 않아도 context의 초기값을 알 수 있다. 물론 인자를 주어도 노상관이다.
import { createContext } from "react";
export const ThemeContext = createContext();
export const UserContext = createContext();
이제 컴포넌트에서 context를 consuming해보면, 크게 두 가지 방법이 있는데 첫 번째는ContextName.Consumer
를 활용하는 방법이고, 두 번째는 useContext를 활용하는 방법이다. 여기서는useContext를 활용한다.
위 App.js
에서 Provider의 자식 컴포넌트로 존재하는 MyComponent
코드를 보면
MyComponent.js
import { useContext } from "react";
import { ThemeContext, UserContext } from "./context";
const THEMES = {
light: {
backgroudColor: "#EEEEEE",
color: "black",
},
dark: {
backgroundColor: "#333333",
color: "white",
},
};
function MyComponent() {
const themeType = useContext(ThemeContext);
const { name } = useContext(UserContext);
// Layout과 Title은 styled components 입니다.
return (
<Layout
backgroudColor={THEMES[themeType].backgroundColor}
color={THEMES[themeType].color}
>
<Title>{`환영합니다 ${name}님!`}</Title>
</Layout>
);
}
Custom Provider Component를 만든다. 위 기생성된 Provider 컴포넌트를 분리한다.
// context.js
export function ContextProvider({ children }) {
return (
<ThemeContext.Provider value="dark">
<UserContext.Provider value={{ name: "Guest123" }}>
{children}
</UserContext.Provider>
</ThemeContext.Provider>
);
}
// App.js
export function Main() {
return (
<ContextProvider>
<MyComponent />
</ContextProvider>
);
}
앞서 말한대로 context는 그 자체만으로 상태가 될 수 없다. 따라서 useState
나 useReducer
와 같은 상태 관리 훅을 활용해 context를 업데이트할 것이다. 물론 useState를 활용해서도 상태변경을 할 수 있다. 그러나 useReducer라는 훅을 활용한 context를 업데이트하는 코드를 보면
context.js
import { createContext, useReducer } from "react";
const initialTheme = "dark";
const initialUser = { name: "Guest" };
export const ThemeContext = createContext();
export const ThemeDispatchContext = createContext();
export const UserContext = createContext();
export const UserDispatchContext = createContext();
function themeReducer(state, action) {
switch (action.type) {
case "TOGGLE":
return state === "light" ? "dark" : "light";
case "DARK":
return "dark";
case "LIGHT":
return "light";
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
function userReducer(state, action) {
switch (action.type) {
case "LOGOUT":
return { ...state, name: initialUser.name };
case "NAME_UPDATE":
return { ...state, name: action.newUserName };
default:
throw new Error(`Unknown action type: ${action.type}`);
}
}
export function GlobalContextProvider({ children }) {
const [theme, themeDispatch] = useReducer(themeReducer, initialTheme);
const [user, userDispatch] = useReducer(userReducer, initialUser);
return (
<ThemeContext.Provider value={theme}>
<ThemeDispatchContext.Provider value={themeDispatch}>
<UserContext.Provider value={user}>
<UserDispatchContext.Provider value={userDispatch}>
{children}
</UserDispatchContext.Provider>
</UserContext.Provider>
</ThemeDispatchContext.Provider>
</ThemeContext.Provider>
);
}
useReducer
부터 보면, useState
의 경우 state를 변경하는 함수를 통해 바로 상태를 변경할 수 있다. useReducer
는 useState
의 대체함수로 새로운 것이 아니다. useReducer
의 경우 반환 값이 [state, dispatch]
의 형태인 것을 알 수 있다. state의 경우 useState의 경우와 같이 상태값이다. 그리고 dispatch가 그 상태를 특정 방식으로만 변경하는 함수를 의미한다.
dispatch는 액션을 발생시키는 함수로 dispatch({ type: "NAME_UPDATE", newUserName: "KIM" })
과 같은 형태로 사용한다. 즉 useReducer
로 생성한 상태는 개발자가 정의한 특정 action 방식으로만 상태를 변경할 수 있는 것이다. 예를 들어 위 theme의 경우 오직 "TOGGLE", "DARK", "LIGHT"라는 세 가지 액션 밖에 없다.
App.js
export default function Main() {
return (
<GlobalContextProvider>
<MyComponent />
</GlobalContextProvider>
);
}
const THEMES = {
light: {
backgroudColor: "#EEEEEE",
color: "black",
togglerColor: "#333333",
},
dark: {
backgroundColor: "#333333",
color: "white",
togglerColor: "orange",
},
};
function MyComponent() {
const themeType = useContext(ThemeContext);
const { name } = useContext(UserContext);
const themeDispatch = useContext(ThemeDispatchContext);
const userDispatch = useContext(UserDispatchContext);
return (
<Layout
backgroudColor={THEMES[themeType].backgroundColor}
color={THEMES[themeType].color}
>
<ThemeToggler
togglerColor={THEMES[themeType].togglerColor}
onClick={() => themeDispatch({ type: "TOGGLE" })}
/>
<Title
onClick={() =>
userDispatch({ type: "NAME_UPDATE", newUserName: "lucas" })
}
>
{`안녕하세요 ${name}님!`}
</Title>
</Layout>
);
}
결과
themeDispatch와 userDispatch의 사용법에 유의하며 봐야한다. ThemeToggler
를 클릭하면 테마가 토글되는데, Title을 클릭하면 "lucas"라는 이름으로 이름이 변경된다. 이 예시에서는 컴포넌트는 하나의 간단한 컴포넌트라 복잡하지 않다. context나 useReducer를 사용하여 코드를 복잡하게 만드는 것이 불필요하게 느껴지는 상황이라면 단순히 컴포넌트의 상태인지 의심하거나 useState를 사용하는 것이 좋을 수도 있다.
좀 더 복잡한 과정을 예상해보면