
리액트에서는 컴포넌트간 상태공유를 할 때, 부모 컴포넌트에서 상태관리를 하면서 자식 컴포넌트에는 상태정보를 props 로 전달한다.
다음과같이 단순한 구조에서는 상태를 공유하고 관리하는 것이 어렵지 않다.

하지만, 상태 끌어올리기 글에서도 다뤘듯이 자식 컴포넌트에서 관리중이던 state 를 부모 컴포넌트로 끌어올려 공유해야하는 상황도 있다.
상태를 끌어올리기만 하는 상황이 있는 것은 아니다. 아래와 같은 상황에서 A컴포넌트에서 C컴포넌트 로 상태를 전달하려면 중간에 C컴포넌트 에 관여하지 않는 컴포넌트에도 props 를 전달하여야 합니다.

내가 필요한 state도 아닌데 전달만하기위해서 props를 받아서 전달을 해야한다면?
물론, 하면 해! 한다면 할 말이 없지만 무척 비효율적인 처리방식이다.
이러한 처리방식을 Props Drilling 이라고한다.
✋🏻Props Drilling 이란?
props를 오로지 하위 컴포넌트로 전달하는 용도로만 사용하는 컴포넌트들을 거치면서 데이터를 전달하는 과정이다.
state를 전달해줄 상위 컴포넌트가 너무 높이 위치해있어서 하위 컴포넌트까지props로 내려가는것이 드릴과 비슷하여 붙여진 이름이지 않을까싶다.
Props Drilling은 비효율적인 부분뿐만 아니라 코드가 복잡해지고 그로인해 유지보수가 어려워지는 문제까지 야기한다.
또한, 컴포넌트 구조가 변경되면 props 전달경로도 함께 변경되어야하므로 효율을 중시하는 개발에서는 질색팔색할 일인 것이다. 🤯

그렇다면 C컴포넌트 에서 필요한 state니까 A컴포넌트 에서 바로 전달하면 편하고 좋지않을까?!
그래서 등장한 것이 리액트의 컨텍스트(Context) 이다.
컨텍스트(Context) 를 사용하면 컨텍스트의 내부에 값을 공유할 수 있다.
| 부모 컴포넌트가 트리 전체에 데이터를 전달할 수 있도록 해주는 것
자세히 살펴보겠지만 간단하게 context가 진행되는 단계는 다음과 같다.
const Context = createContext(defaultValue)
// 레거시 방식 (사용권장 ❌)
function Button() {
return (
<ThemeContext.Consumer> // .Counsumer는 생략불가능 ❌
//...
</ThemeContext.Consumer>
)
}
// useContext 방식 (사용권장 ✅)
function Button() {
const theme = useContext(ThemeContext)
return <button className={theme} />
function App() {
const [theme, setTheme] = useState('light')
// ...
return (
<ThemeContext.Provider value={theme}> // .Provider 생략가능 ✅
<Page />
</ThemeContext.Provider>
}
createContext() 로 생성한 컨텍스트는 완벽하게 분리되어 있기 때문에 서로 다른 리액트 context는 서로에게 영향을 주지 않는다. → 하나의 컴포넌트에서 여러 컨텍스트를 사용 또는 제공할 수 있다.1. 상태관리
2. 테마관리
3. 인증관리
4. 라우팅관리
이제 위에서 간단하게 살펴봤던 context 의 진행단계에 대해서 살펴보자.
컨텍스트를 생성하여 사용하려면 먼저 createContext를 import 해야한다.
이를 통해 컨텍스트 객체를 만들 수 있는데, 생성된 컨텍스트 객체는 공급자(Provider) 와 소비자(Consumer) 를 포함한다.
생성시 매개변수로 defaultValue를 받는데, 이 값은 상위에 일치하는 컨텍스트 제공자가 없을 경우 컨텍스트가 가지는 기본값이다.
의미있게 설정할 기본값이 없으면 null 로 설정하면되며, 이 값은 정적인 값으로 시간이 지나도 변경되지 않는다.
생성을 하면 위에서 언급한 컨텍스트 객체를 반환하게 되는데, 객체 자체는 어떠한 정보도 가지고 있지 않다.
대신 다음의 몇 가지 속성을 가지고 있다.
SomeContext : 컴포넌트에 컨텍스트 값을 제공한다.SomeContext.Consumner : 컨텍스트 값을 읽는 방식이며, useContext가 등장하기 전의 방식이다. (SomeContext.Provider : 컨텍스트 제공자로, 컴포넌트를 감싸서 컨텍스트의 값을 모든 내부 컴포넌트에 지정한다. (반환된 컨텍스트 객체를 컴포넌트가 useContext()에 전달하여 컨텍스트 값을 읽을 수 있게 한다.
// 기본 형태
import { createContext } from 'react'
export const MyContext = createContext(defaultValue)
import { createContext } from 'react'
export const THEME = {
light: {
foreground: '#000',
background: '#fff',
},
dark: {
foreground: '#fff',
background: '#000',
}
}
// 다른 컴포넌트에서 사용할 수 있도록 export
export const ThemeContext = createContext() // createContext의 유일한 인자는 '기본값'임
console.log(ThemeContext.Provider) // 공급자
console.log(ThemeContext.Consumer) // 소비자
컨텍스트를 제공하지 않으면 리액트는 생성단계에서 지정한 기본값을 가져온다.
생성한 컨텍스트를 자식 컴포넌트에게 제공하기 위해서는 provider 가 필요하다.
생성한 컨텍스트를 사용하려면 useContext 라는 리액트 훅이 필요하다.
더불어 생성한 컨텍스트를 사용하려는 컴포넌트에서는 useContext 훅과 함께 생성 컨텍스트도 필요하다.
import { useContext } from 'react'
import { ThemeContext } from '파일위치'
export default function Toolbar() {
const [mode] = useContext(ThemeContext) // 컨텍스트 값 읽기
// ...
컴포넌트 내부에 작성된 컨텍스트 로직을 별도의 파일로 분리하여 사용 및 관리하는 것이 유지보수 측면에서도, 코드의 가독성 측면에서도 좋다.
상태와 상태 업데이트 함수를 값으로 제공한다.✋🏻잠깐! 지금까지 컨텍스트에 대해 살펴봤는데 그래서 useContext가 뭔데?
| 컴포넌트에서 Context 를 읽고 구독할 수 있는 리액트 훅이다.
const value = useContext(SomeContext)
SomeContext : createContext 로 생성한 컨텍스트로 자체적으로 정보를 담고 있지는 않다. 참고SomeContext에 전달된 값으로 결정된다.defaultValue가 되며, 반환된 값은 항상 최신상태이다.
useContext()는 항상 호출하는 컴포넌트 상위에서 가장 가까운 Provider 를 찾는다. 위쪽 방향으로 찾으며, 호출하는 컴포넌트 내의 Provider 는 고려하지 않는다.
context 를 업데이트하려면 State와 함께 시너지효과를 내보자!
🔗 참고자료
https://ko.react.dev/reference/react/createContext
https://ko.react.dev/reference/react/useContext
https://ko.react.dev/learn/passing-data-deeply-with-context
https://ko.react.dev/learn/scaling-up-with-reducer-and-context#moving-all-wiring-into-a-single-file
https://kentcdodds.com/blog/prop-drilling