리액트에서의 context 가 무엇인지 아는가?
일반적으로 리액트에서 부모 컴포넌트에서 자식 컴포넌트로 정보를 전달할 때 props 를 통해 정보를 전달한다. 그런데 만약 중간에 많은 컴포넌트가 있거나 같은 정보를 필요로 하는 컴포넌트가 많을 때 props 를 전달하는 것은 복잡해질 수 있다.
가장 가까운 공통 조상은 해당 데이터를 필요로 하는 컴포넌트들로부터 멀리 떨어질 수 있고 lifting state up
은 해당 데이터를 얻기 위해 컴포넌트가 계층 구조를 거치며 prop drilling
이라고 불리는 번거로운 상황을 초래할 수 있다.
props 를 순서대로 전달할때 | 글로벌 state 를 사용할 때 |
---|---|
![]() | ![]() |
이미지 출처 : 모던 자바스크립트로 배우는 리액트 입문
버킷 릴레이
- 불필요하게 props 를 전달하는 것을 의미한다.
- 버킷 릴레이 방식은 중간에 props 를 전달하는 컴포넌트들도 props 로 아무것도 하지 않고 전달만 할지라도 props 가 변경되면 재렌더링된다.
lifting state up | prop drilling |
---|---|
![]() | ![]() |
lifting state up
- 리액트에서 두 개 이상의 컴포넌트가 상태 변화를 동시에 처리하고 싶을때
state
를 각 컴포넌트에서 제거하고 가장 가까운 공통 부모 컴포넌트로state
를 옮긴 후 props 를 통해 그들에게 내려 전달하는 방식을 말한다.
prop drilling
- 리액트 애플리케이션에서 데이터를 전달하는 과정에서 발생할 수 있는 문제를 가리키는 용어다.
context
는 부모 컴포넌트가 정보를 전달하고자 할때 그 대상이 어떤 컴포넌트이든지, 얼마나 깊든지 간에 (중간에 컴포넌트가 많다는 의미) props 로 전달할 필요 없이 정보를 마치 텔레포트하듯이 전달할 수 있게 한다.
컨텍스트는 부모컴포넌트에게 트리의 자식 컴포넌트로 데이터를 전달하는 방법을 제공한다.
현재 코드를 보면 Heading 이 어떤 헤딩 태그를 가질지 h1/h2/.../h6
결정하기 위해 모든 Heading 에 props 로 level 을 전달하고 있다.
그러는 대신 Heading
에서 props 를 제거하고 Section
에서 level prop 을 전달하는 것이 더 좋아보인다.
<Section level={3}>
<Heading>About</Heading>
<Heading>Photos</Heading>
<Heading>Videos</Heading>
</Section>
그럼 이제 Heading
컴포넌트는 가장 가까운 Section
컴포넌트로부터 어떻게 level 에 대해 알 수 있을까? Heading
같은 자식 컴포넌트들은 트리에서 데이터를 갖는 컴포넌트에게 데이터에 대해 물어볼 방법이 필요하다.
이때 context 가 사용된다.
이미지 출처: react.dev passing data deeply with context
codesandbox 코드는 오른쪽의 바를 왼쪽으로 드래그하면 실행화면을 볼 수 있다.
Create
- context
를 생성
한다.
Use
- 데이터를 필요로하는 컴포넌트에서 해당 context
를 사용
한다.
Provide
- 데이터를 갖는 컴포넌트는 context
를 제공
한다.
react 는 createContext
를 제공하여 context
를 생성할 수 있도록 한다. createContext
로 만든 Context
는 해당 파일로부터 export 해줘야 다른 컴포넌트에서 이 context
를 사용할 수 있다.
코드 출처 : react.dev - passing data deeply with context
createContext
가 갖는 유일한 인자는 기본값이다. 이 인자로object
를 포함하여 어느 종류의 값도 전달할 수 있다.
데이터를 필요로하는 컴포넌트에서 useContext
훅과 앞에서 생성한 context
를 import 한다.
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
위의 예시에서 Heading 컴포넌트가 level 이라는 data 를 사용하고자 하는 컴포넌트에 해당한다.
앞에서 Heading 컴포넌트는 level 을 props 로 전달받고 있었으나 이를 제거하고 useContext에 미리 생성한 context
를 인자로 전달한다.
// 기존 Heading 컴포넌트
export default function Heading({ level, children }) {
// ...
}
// 수정한 Heading 컴포넌트
export default function Heading({ children }) {
const level = useContext(LevelContext);
// ...
}
useContext
훅은 React 에게Heading
컴포넌트가LevelContext
라는context
를 읽고 싶어한다고 말해준다.
useContext 를 사용한 뒤의 컴포넌트
context
를 생성하고 사용하고자 했다. 그러나 아직context
를 사용할 수 없다. 그러기 전에context
를 먼저provide
해줘야 한다. 그렇지 않으면LevelContext
에서 기본값으로 넣은 1 이 모든Heading
의level
이 된다.
위의 예시에서 Section
컴포넌트는 children 을 렌더링하고 있다.
export default function Section({ children }) {
return (
<section className="section">
{children}
</section>
);
}
이 컴포넌트를 context provider
로 감싸서 LevelContext
를 provide
할 수 있도록 하자.
import { LevelContext } from './LevelContext.js';
export default function Section({ level, children }) {
return (
<section className="section">
<LevelContext.Provider value={level}>
{children}
</LevelContext.Provider>
</section>
);
}
이는 React 에게 만약
Section
컴포넌트 내에LevelContext
에 대해 물어보는 컴포넌트가 있다면 그 컴포넌트에게level
을 전달하라고 알려준다.
전체 흐름
:
1.Section
에게 level prop 전달한다.
2.Section
은<LevelContext.Provider value={level}>
로children
을 감싼다.
3.Heading
은useContext(LevelContext)
를 갖고LevelContext
의 value 에 대해 물어본다.
react 공식문서 에서 create - use - provide 순서로 useContext 를 설명하는데 개인적으로는 흐름상 create - provide - use 순서로 설명하는게 더 자연스러워 보였다.
최종 형태
context 가 사용하기 쉬워서 과하게 사용하게 될 수 있다. 어떤 props 을 몇 단계 깊이로 전달한다는게 반드시 이 정보를 context 에 넣어야 한다는 것을 의미하지는 않는다.
passing props 로 시작하자
:
전달하는 prop 이 많지 않을때가 더 흔한 상황이다. 때로는 prop 로 전달하는게 어떤 데이터를 사용하는 지 분명하게 보이기도 한다.
component 를 추출하고 JSX 를 children 으로 전달하자
:
<Layout posts={posts} />
와 같이 post라는 데이터를 직접 사용하지 않는 경우 Layout 이 children 을 prop 으로 받아 <Layout><Posts posts={posts} /></Layout>
로 처리하면 불필요한 layer 를 줄일 수 있다.
테마 설정
: 사용자가 앱의 외관을 변경할 수 있는 경우 앱의 맨 위에 context provider 를 배치하고 시각적인 모양을 조정해야 하는 경우 컴포넌트에서 context 를 사용할 수 있다.
현재 계정
: 많은 컴포넌트가 현재 로그인한 유저들에 대해 알아야 할 때가 있을 수 있다. 이걸 컨텍스트에 넣으면 트리의 어디에서나 편하게 읽을 수 있다.
라우팅
: 대부분의 라우팅 솔루션은 현재 경로를 보유하기 위해 내부적으로 context 를 사용한다.
상태 관리
: 앱의 규모가 커지며 앱의 상단에 많은 상태가 몰리게 될 것이다. 하위의 많은 먼 컴포넌트에서도 이를 변경하고 싶어할 수 있다. 보통 복잡한 상태를 관리하고 하위의 먼 컴포넌트로 전달하기 위해 reducer 를 context 와 함께 사용한다.
Context 객체 하나의 값이 바뀌었을 때
useContext
로Context
를 참조하고 있는 컴포넌트는 모두 재렌더링된다.
따라서 하나의 context 에 다양한 state 를 함께 두는 것은 피해야 한다.
useContext
훅은 컴포넌트에서 context
를 읽고 subscribe 할 수 있도록 한다.
const value = useContext(SomeContext)
someContext
: 미리 createContext
로 생성한 context
. 일반적으로는 context 자체만으로는 정보를 갖고 있지 않고 컴포넌트로부터 provide 혹은 읽을 정보의 종류를 나타낸다.context
를 읽고 subscribe 하기 위해 useContext
를 컴포넌트의 상단에서 호출하자.
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext);
// ...
useContext
는 context value
를 반환한다. 이 value 는 가장 가까운 SomeContext.Provider
로부터 전달받아 결정된다. Provider
는 가장 가까운 상위 컴포넌트에서 찾고 useContext
를 호출하고 있는 컴포넌트의 provider
는 고려하지 않는다.useContext()
로 반환하는 context value
는 createContext
로 전달한 default value
가 된다.context value 로 object, function 을 포함한 어느 값도 전달할 수 있다.
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}
return (
<AuthContext.Provider value={{ currentUser, login }}>
<Page />
</AuthContext.Provider>
);
}
function 의 경우 리액트가 매번 재렌더링 할 때마다 새로운 함수를 생성하므로 참조하는 값이 달라진다. 그러면 useContext 를 호출하고 있는 모든 컴포넌트는 재렌더링이 된다.
이때 currentUser 의 경우엔 변하지도 않았는데 재렌더링을 할 필요가 없다.
이 상황에서 리액트는 재렌더링을 최적화하기 위해 useCallback 으로 login 함수를 감싸고 contextValue 를 useMemo 로 감싸서 코드를 개선할 수 있다.
import { useCallback, useMemo } from 'react';
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
const login = useCallback((response) => {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}, []);
const contextValue = useMemo(() => ({
currentUser,
login
}), [currentUser, login]);
return (
<AuthContext.Provider value={contextValue}>
<Page />
</AuthContext.Provider>
);
}
이전 포스트에서 useCallback, useMemo 에 대해 정리한 글을 다시 참고했다.
useCallback
: 함수 메모이제이션. 재렌더링 사이에 함수 정의를 캐시할 수 있게 해준다. 위의 코드에선 로그인은 첫 렌더링 이후 바뀌지 않을 것을 상정하여 dependencies 에 빈 배열을 넣었다.useMemo
: 변수 메모이제이션. 재렌더링 사이에 계산의 결과를 캐시해준다.
위의 코드의 경우 currentUser
가 바뀌지 않는 한 useContext(AuthContext)
는 재렌더링하지 않는다.
useReducer 에 대한 학습을 진행한 후 React 가 reducer 와 context 를 사용하여 어떻게 상태관리를 하고 있는지 정리하자.
book
모던 자바스크립트로 배우는 리액트 입문
docs
react.dev - context 로 데이터를 깊이 전달