Context Hooks
Context란?
React에서 컴포넌트가 데이터를 다루는 방법에는 우리가 앞서 살펴본 props와 state 외에도 context라는 기능이 있습니다.
context는 데이터의 흐름이 부모 컴포넌트로부터 자식 컴포넌트에게 전달되는 props와 state와는 달리 데이터의 흐름과 상관없는 전역적인 데이터를 다룰 때 사용할 수 있습니다. 즉, context를 사용하면 사용자의 계정 정보나 설정 파일 등 해당 애플리케이션에 포함된 모든 컴포넌트에서 접근할 필요가 있는 데이터를 손쉽게 관리할 수 있습니다.
조금 복잡한 설정을 통해 Redux와 같은 전역 상태 관리 라이브러리를 활용하여 context를 사용할 수도 있지만, Context API나 useContext Hook을 사용하여 좀 더 손쉽게 context를 사용할 수 있습니다.
Prop Drilling
React에서는 컴포넌트에 데이터를 전달해야 할 경우에 일반적으로 prop을 사용하여 데이터를 전달하게 됩니다.
하지만 prop은 데이터를 상위 컴포넌트에서 하위 컴포넌트 방향으로만 전달할 수 있습니다. 따라서 컴포넌트 트리의 한 부분에서 다른 부분으로 데이터를 전달해야 할 경우 prop을 전달하기 위한 하위 컴포넌트들을 거치면서 데이터가 전달되며, 이러한 과정을 Prop Drilling이라고 부릅니다.
Prop Drilling은 prop을 전달할 때 거쳐야 할 컴포넌트의 개수가 적으면 전혀 문제가 되지 않습니다. 하지만 prop이 거쳐야 할 컴포넌트의 개수가 10개, 20개가 넘어가게 되면 코드를 통해 해당 prop을 추적하는 것이 어려워지며, 코드의 유지보수 또한 힘들어집니다. 또한, 거쳐야 하는 모든 컴포넌트에서 prop을 설정해줘야 하기 때문에 개발자가 실수를 할 확률도 높아집니다.
Prop Drilling 예시
const App = () => {
return <FirstComponent content="Hello, React!" />;
};
const FirstComponent = ({ content }) => {
return <SecondComponent content={content} />;
};
const SecondComponent = ({ content }) => {
return <ThirdComponent content={content} />;
};
const ThirdComponent = ({ content }) => {
return <ComponentRequiringData content={content} />;
};
const ComponentRequiringData = ({ content }) => {
return <h1>{content}</h1>;
};
export default App;
이러한 Prop Drilling을 피하기 위해서는 전역 상태 관리 라이브러리를 사용하거나 Context API를 활용하여 데이터가 필요한 컴포넌트에서 직접 해당 값에 접근할 수 있도록 구현할 수 있습니다.
Context API를 활용하여 Context 사용하기
React 패키지의 createContext() 함수를 불어와 호출함으로써 context를 생성할 수 있습니다.
Context 생성하기
import { createContext } from "react";//...const SomeContext = createContext(defaultValue)
이렇게 생성된 context 객체에는 Provider라는 컴포넌트가 포함되어 있습니다.
Provider 컴포넌트에 value prop으로 데이터를 전달하면 Provider 컴포넌트의 하위 컴포넌트에서는 해당 값에 바로 접근할 수 있습니다. 이렇게 Provider 컴포넌트로부터 값을 전달 받을 수 있는 컴포넌트의 개수에는 제한이 없으며, Provider 컴포넌트의 하위 레벨에 또 다른 Provider 컴포넌트를 배치하는 것도 가능합니다. 단, 이 경우에는 하위 레벨의 Provider 값이 우선 시 됩니다
Context 설정하기
<SomeContext.Provider value={/* 특정 값 */}> // SomeContext에 접근할 수 있는 하위 컴포넌트들의 위치 </SomeContext.Provider>
이렇게 설정한 context를 구독하기 위해서는 Consumer 컴포넌트를 사용합니다.
Consumer 컴포넌트의 children은 반드시 함수이어야 하며, 이 함수는 context의 현재값을 인수로 전달받고 React 엘리먼트를 반환합니다.
이 함수가 전달 받는 value 매개변수의 값은 해당 context의 Provider 컴포넌트 중에서 Consumer 컴포넌트와 가장 가까운 상위 Provider 컴포넌트의 value prop 값과 동일한 값으로 설정됩니다.
만약 Consumer 컴포넌트의 상위 레벨에 Provider 컴포넌트가 존재하지 않는다면 value 매개변수의 값은 createContext() 함수를 호출할 때 전달한 초깃값(defaultValue)으로 자동 설정됩니다.
Context 구독하기
<HelloContext.Consumer>
{value => /* Context 값을 이용한 렌더링 */}
</HelloContext.Consumer>
이제 앞서 살펴본 Prop Drilling 예제를 Context API를 사용하도록 변경해 봅시다.
예제(App.js)
import { createContext } from "react";
const HelloContext = createContext();
const App = () => {
return (
<HelloContext.Provider value="Hello, React!">
<FirstComponent />
</HelloContext.Provider>
);
};
const FirstComponent = () => {
return <SecondComponent />;
};
const SecondComponent = () => {
return <ThirdComponent />;
};
const ThirdComponent = () => {
return <ComponentRequiringData />;
};
const ComponentRequiringData = () => {
return (
<HelloContext.Consumer>{(value) => <h1>{value}</h1>}</HelloContext.Consumer>
);
};
export default App;
useContext Hook을 활용하여 Context 사용하기
React Hook 중에서 useContext Hook을 활용해도 context를 손쉽게 사용할 수 있습니다. 단, useContext Hook은 함수 컴포넌트에서만 사용이 가능하고, 만약 클래스 컴포넌트에서 context를 사용하고 싶다면 Context API를 활용해야 합니다.
useContext를 사용할 때도 Context API와 동일하게 createContext() 함수를 호출하여 context를 생성합니다. 이렇게 생성된 context를 읽고 구독하려면 해당 컴포넌트에서 useContext를 호출하면 됩니다.
useContext 문법
const value = useContext(SomeContext)
useContext는 context 객체를 인수로 전달 받아 해당 context의 현재 값을 반환하며, useContext를 호출한 컴포넌트는 해당 context의 값이 변경될 때마다 리렌더링됩니다.
이제 앞서 Context API를 활용한 예제를 useContext Hook을 사용하도록 변경해 봅시다.
App.js
import { createContext, useContext } from "react";
const HelloContext = createContext();
const App = () => {
return (
<HelloContext.Provider value="Hello, React!">
<FirstComponent />
</HelloContext.Provider>
);
};
//...
const ComponentRequiringData = () => {
const value = useContext(HelloContext);
return <h1>{value}</h1>;
};
export default App;
마지막으로 context를 어떤 방식으로 사용하는지를 좀 더 자세히 알아보기 위해 context를 사용하여 Box 컴포넌트의 스타일을 변경하는 조금 복잡한 예제를 살펴보도록 합시다.
styles.css
/* light와 dark 공통 스타일 */
.box-light,
.box-dark {
border: 1px solid black;
padding: 10px;
margin-bottom: 10px;
}
/* light 스타일 */
.box-light {
color: black;
background: white;
}
/* dark 스타일 */
.box-dark {
color: white;
background: black;
}
예제(App.js)
import { createContext, useContext, useState } from "react";
import "./styles.css";
// ThemeContext 객체를 생성하고, 기본값을 'light'로 설정함
const ThemeContext = createContext("light");
const App = () => {
// theme를 저장하기 위한 state를 생성하여 초깃값을 'light'로 설정함
const [theme, setTheme] = useState("light");
return (
<>
{/* ThemeContext를 구독하고 있는 하위 레벨의 컴포넌트들에게 value값을 전달함 */}
<ThemeContext.Provider value={theme}>
<Box text="Hello, React!">테마를 변경해 봅시다!</Box>
</ThemeContext.Provider>
<button
onClick={() => {
setTheme(theme === "dark" ? "light" : "dark");
}}
>
테마 변경하기
</button>
</>
);
};
const Box = ({ text, children }) => {
//ThemeContext를 theme라는 이름으로 구독함
const theme = useContext(ThemeContext);
const className = "box-" + theme;
return (
<section className={className}>
<h1>{text}</h1>
{children}
</section>
);
}
export default App;