Context
React 에서 데이터는 위에서 아래로 props를 통해 전달합니다.
따라서, 여러 컴포넌트들에 전해줘야 하는 경우(선호 로케일, UI 테마), 컴포넌트에서 사용하고 있는 데이터를 현재의 데이터 흐름에 넣고 싶은 경우 등의 과정은 번거로울 수 있습니다.
Context를 이용하면, 트리 단계마다 명시적으로 props를 넘겨주지 않아도 많은 컴포넌트가 데이터를 공유하도록 할 수 있습니다.
즉, 부모 컴포넌트로부터 자식 컴포넌트로 전달되는 데이터 흐름과는 상관없이 전역적으로 데이터를 다룰 때 사용합니다.
Context는 전역 데이터를 Context에 저장한 후, 데이터가 필요한 컴포넌트에서 해당 데이터를 불러와 사용할 수 있습니다.
Context API
React에서 Context를 사용하기 위해서는 Context API를 사용해야 하며, Context의 Provider와 Consumer를 사용해야 합니다.
Context에 저장된 데이터를 사용하기 위해서는 공통 부모 컴포넌트에 Context의 Provider를 사용하여 데이터를 제공해야 하며, 데이터를 사용하려는 컴포넌트에서 Context의 Consumer를 사용하여 실제로 데이터를 사용합니다.
Context API 사용 방법
- createContext 메서드를 사용하여 context 생성한다.
- 생성한 context를 대상 컴포넌트에 값을 내려주기 위해서 Provider로 대상 컴포넌트를 감싼다.
- Provider의 프로퍼티인 value에 전달할 데이터를 넣는다.
- Provider의 value에 담은 데이터를 전달 할 때는, 2가지 방식으로 전달이 가능하다. Consumer 컴포넌트 또는 useContext라는 훅을 이용하는 법이다.
React.createContext(defaultValue)
Context 객체를 생성하는 역할을 합니다.
개념적으로 React Context는 전역 데이터를 담고 있는 하나의 저장 공간이라고 생각할 수 있습니다.
Provider 컴포넌트
context를 구독하는 컴포넌트들에게 context의 변화를 알리는 역할을 합니다.
Provider 컴포넌트는 value prop을 받아서 이 값을 하위에 있는 컴포넌트에게 전달합니다.
Provider 하위에 또 다른 Provider를 배치하는 것도 가능하며, 이 경우 하위 Provider의 값이 우선시됩니다.
Provider 하위에서 context를 구독하는 모든 컴포넌트는 Provider의 value prop가 바뀔 때마다 다시 렌더링 됩니다.
Consumer
context 변화를 구독하는 React 컴포넌트입니다.
이 컴포넌트를 사용하면 함수 컴포넌트안에서 context를 구독할 수 있습니다.
Context.Consumer의 자식은 함수여야합니다.
이 함수는 context의 현재값을 받고 React 노드를 반환합니다.
상위에 Provider가 없다면 value 매개변수 값은 createContext()에 보냈던 defaultValue와 동일할 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createContext } from ‘react’;
export const themeContext = createContext(전달할 데이터의 초기값);
export default function App() {
return (
<themeContext.Provider value={전달할 데이터}>
<Theme />
</themeContext.Provider>
)
}
function Theme() {
return (
<themeContext.Consumer>
{value => <div>{value}</div>}
</themeContext.Consumer>
)
}
useContext()
React Hooks에서 16.8 버전부터 추가된 useContext 함수를 이용하면 좀 더 깔끔하게 Context에 저장되어 있는 전역 데이터에 접근할 수 있습니다.
단, 이 방법은 함수 컴포넌트에서만 사용 가능합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createContext, useContext } from ‘react’;
export const themeContext = createContext(전달할 데이터의 초기값);
export default function App() {
return (
<themeContext.Provider value={전달 데이터}>
<Theme />
</themeContext.Provider>
)
}
function Theme() {
const theme = useContext(themeContext);
return <div>{theme}</div> // Provider에서 value로 전달한 데이터 출력
}
Context를 언제 사용할까?
여러 개의 컴포넌트들이 접근해야 하는 데이터
(로그인 여부, 로그인 정보, 현재 언어, UI 테마 등)
이전에는 목표 컴포넌트까지 부모 컴포넌트로부터 반복해서 props로 넘겨주어야했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function App(props) {
return <Toolbar theme="dark" />;
}
function Toolbar(props) {
// 중간 컴포넌트들(Toolbar)은 불필요한 테마 prop를 받아서
// 목표 컴포넌트 (ThemeButton)에 전달해야 합니다.
// 깊이가 깊어질수록 이러한 비효율적인 반복은 코드가 복잡해질 것입니다.
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
function ThemedButton(props) {
return <Button theme={props.theme} />;
}
Context를 활용하면 비효율적인 반복없이 간편하게 데이터를 넘겨줄 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const ThemeContext = React.createContext("light");
function App(props) {
return (
// Provider를 이용해 하위 트리에 테마 값을 보내줍니다.
// 아무리 깊숙히 있어도, 모든 컴포넌트가 이 값을 읽을 수 있습니다.
<ThemeContext.Provider value="dark">
<Toolbar theme="dark" />;
</ThemeContext.Provider>
);
}
function Toolbar(props) {
// 중간 컴포넌트가 일일이 props를 넘겨줄 필요가 없습니다.
return (
<div>
<ThemedButton />
</div>
);
}
function ThemedButton(props) {
return (
<ThemeContext.Consumer>
{value => <Button theme={value} />};
<ThemeContext.Consumer>
)
}
Context 사용 전 고려할 점
context를 사용하면 컴포넌트를 재사용하기가 어려워진다는 것을 기억합시다.
여러 레벨에 걸쳐 props 넘기는 걸 대체하는 데에는 context보다 컴포넌트 합성이 더 간단한 해결책일 수도 있습니다.
1
2
3
4
5
6
7
8
9
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<NavigationBar user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
위 코드는 여러 단계 아래에 있는 Link 와 Avatar 컴포넌트에게 user 와 avatarSize 라는 props를 전달해야합니다.
실제로 사용되는 곳은 Avatar 컴포넌트 뿐인데 user와 avatarSize props를 여러 단계에 걸쳐 보내줘야합니다. (props drilling)
또한, 위에서 Avatar 컴포넌트로 보내줘야하는 props가 추가된다면 그 또한 중간 레벨에 모두 추가해줘야 합니다.
이 떄, Avatar 컴포넌트 자체를 넘겨주면 Context를 사용하지 않고 이를 해결할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Page(props) {
const user = props.user;
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
);
return <PageLayout userLink={userLink} />;
}
<Page user={user} avatarSize={avatarSize} />
// ... 그 아래에 ...
<PageLayout userLink={...} />
// ... 그 아래에 ...
<NavigationBar userLink={...} />
// ... 그 아래에 ...
{props.userLink}
위 코드는 Link와 Avatar 컴포넌트가 user 와 avatarSize props를 쓴다는 걸 알아야 하는 건 가장 위에 있는 Page 뿐입니다.
최상위 컴포넌트의 제어력은 더 커지기 때문에 더 깔끔한 코드를 쓸 수 있지만 로직이 복잡해질수록 상위 컴포넌트들은 난해해지고 하위 컴포넌트들은 너무 가벼워질 수 있으니 주의해야합니다.