https://ko.legacy.reactjs.org/docs/context.html
Summary
React의 context를 이용하면 컴포넌트 트리 전체에 데이터를 제공할 수 있으며, props를 일일이 넘겨주지 않아도 됩니다. context를 사용하면 컴포넌트 트리 안에서 전역적인 데이터를 공유할 수 있습니다. context는 변하는 데이터를 여러 하위 컴포넌트에 효율적으로 전달하는 방법을 제공합니다.
Facts
- 🌍 context는 React 컴포넌트 트리 안에서 전역적으로 데이터를 공유하는 방법입니다.
- 📦 Context 객체는 React.createContext(defaultValue)를 통해 생성됩니다. defaultValue는 Provider가 값을 제공하지 않을 때 사용되는 값입니다.
- 🔄 Provider 컴포넌트는 context를 구독하는 컴포넌트에게 context의 변화를 알리는 역할을 합니다. value prop을 통해 값을 전달합니다.
- Provider 하위에 Provider가 있는 경우 하위 Provider의 값이 우선시 됨.
- context를 구독하는 모든 컴포넌트는 Provider의
value
prop이 변경될때마다 리렌더링됨. context 값이 변경되었는지 여부는Object.is
와 같은 알고리즘 사용
- 👨💼 Class 컴포넌트에서 context를 이용하려면 Class.contextType을 지정하고 this.context를 사용하여 값을 읽을 수 있습니다.
- 🔍 Context.Consumer는 context 변화를 구독하는 컴포넌트로 함수 컴포넌트 안에서 context를 구독할 수 있게 합니다.
```jsx
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
- 🔮 context 값을 변화시키려면 context를 관리하는 컴포넌트에서 state와 업데이트 메서드를 제공하여 하위 컴포넌트가 값을 업데이트할 수 있도록 합니다.
- 🔄 여러 개의 context를 함께 사용할 때는 Context.Consumer를 중첩하여 사용하거나, render prop 컴포넌트를 활용할 수 있습니다.
### 언제 context를 써야 할까
- context는 주로 컴포넌트 트리 전체에 걸쳐서 변하는 데이터를 공유해야 할 때 사용됩니다.
- context를 사용하면 컴포넌트를 재사용하기가 어려워질 수 있으므로, 신중하게 사용해야 합니다.
- 컴포넌트 합성을 통해 props를 넘기는 것이 더 간단한 경우도 있습니다.
- 복잡한 컴포넌트 트리에서 context는 중간 레벨에 있는 컴포넌트들에게 데이터를 전달할 때 유용합니다.
- context를 사용하기 전에 컴포넌트 합성과 역전 제어 패턴을 고려해보세요.
### API
- React.createContext: Context 객체를 생성하며 defaultValue를 설정할 수 있습니다.
- Context.Provider: context의 변화를 알리고 값을 하위 컴포넌트에 전달하는 역할을 합니다.
- Class.contextType: Class 컴포넌트에서 context 값을 읽어올 수 있도록 해주는 프로퍼티입니다.
- Context.Consumer: context 값을 구독하는 컴포넌트를 정의할 때 사용됩니다.
- Context.displayName: 개발자 도구에서 context를 표시할 때 사용되는 이름입니다.
### 주의사항
- 예전 버전의 context API가 존재했으나, 새로운 API로 옮기는 것이 권장되며 예전 API는 삭제될 예정입니다.
- context 값을 업데이트할 때 참조(reference)를 확인하여 불필요한 렌더링을 방지하세요. 부모의 state로 값을 끌어올려 사용할 수도 있습니다.
```js
// BAD
class App extends React.Component {
render() {
return (
<MyContext.Provider value=>
<Toolbar />
</MyContext.Provider>
);
}
}
// GOOD
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
value: {something: 'something'},
};
}
render() {
return (
<MyContext.Provider value={this.state.value}>
<Toolbar />
</MyContext.Provider>
);
}
}
useContext
https://react.dev/reference/react/useContext
함수형 컴포넌트에서 context를 구독하는 방법
1
2
3
4
5
6
7
import { useContext } from 'react';
function MyComponent() {
const theme = useContext(ThemeContext);
// ...
- Parameter
SomeContext
: createContext로 만든 Context.
- Returns
SomeContext.Provider
의 value props에 넣은 context value를 반환함.- Provider가 없으면
defaultValue
에 넣은 값이 반환됨. - 이 반환되는 값은 항상 최신값이며, 변경시 리렌더링된다.
- Caveats
- 같은 컴포넌트에서 반환된 Provider의 영향을 받지 않는다. Provider는 항상
useContext()
가 사용된 컴포넌트보다 상위에 있어야함 Object.is
로 비교해서 값이 달라졌으면 리렌더링한다.memo
를 통해 리렌더링을 무시하는 것은 children이 새로운 context value를 받는 걸 막지 못한다.- 빌드시스템이 중복 모듈을 만들면 context가 꺠질 수 있다. context를 통해 무언가를 넘기는 것은 context를 제공하는
SomeContext
와 구독할때 사용하는SomeContext
가 정확하게 같은 객체일때만 동작한다. (===
으로 같아야함)
- 같은 컴포넌트에서 반환된 Provider의 영향을 받지 않는다. Provider는 항상
context updating example
- Provider에서 업데이트 ```js import { createContext, useContext, useState } from ‘react’;
const ThemeContext = createContext(null);
export default function MyApp() { const [theme, setTheme] = useState(‘light’); return ( <ThemeContext.Provider value={theme}> <Form /> </ThemeContext.Provider> ) }
function Form({ children }) { return (
function Panel({ title, children }) { const theme = useContext(ThemeContext); const className = ‘panel-‘ + theme; return ( <section className={className}> <h1>{title}</h1> {children} </section> ) }
function Button({ children }) { const theme = useContext(ThemeContext); const className = ‘button-‘ + theme; return ( <button className={className}> {children} </button> ); }
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
- `value`에 업데이트 함수 포함
```js
import { createContext, useContext, useState } from 'react';
const CurrentUserContext = createContext(null);
export default function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
return (
<CurrentUserContext.Provider
value=
>
<Form />
</CurrentUserContext.Provider>
);
}
function Form({ children }) {
return (
<Panel title="Welcome">
<LoginButton />
</Panel>
);
}
function Panel({ title, children }) {
return (
<section className="panel">
<h1>{title}</h1>
{children}
</section>
)
}
function LoginButton() {
const {
currentUser,
setCurrentUser
} = useContext(CurrentUserContext);
if (currentUser !== null) {
return <p>You logged in as {currentUser.name}.</p>;
}
return (
<Button onClick={() => {
setCurrentUser({ name: 'Advika' })
}}>Log in as Advika</Button>
);
}
function Button({ children, onClick }) {
return (
<button className="button" onClick={onClick}>
{children}
</button>
);
}
Optimizing re-renders when passing objects and functions
다음과 같이 value
에 객체를 넣으면 MyApp
이 리렌더링 될 때마다 새로운 객체가 만들어져, 이 context를 구독하고 있는 컴포넌트들도 리렌더링 되게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function MyApp() {
const [currentUser, setCurrentUser] = useState(null);
function login(response) {
storeCredentials(response.credentials);
setCurrentUser(response.user);
}
return (
<AuthContext.Provider value=>
<Page />
</AuthContext.Provider>
);
}
이를 최적화하기 위해서는 다음과같이 useCallback
과 useMemo
를 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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>
);
}
Context refactoring
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);
export default function MyApp() {
const [theme, setTheme] = useState('light');
return (
<MyProviders theme={theme} setTheme={setTheme}>
<WelcomePanel />
<label>
<input
type="checkbox"
checked={theme === 'dark'}
onChange={(e) => {
setTheme(e.target.checked ? 'dark' : 'light')
}}
/>
Use dark mode
</label>
</MyProviders>
);
}
function MyProviders({ children, theme, setTheme }) {
const [currentUser, setCurrentUser] = useState(null);
return (
<ThemeContext.Provider value={theme}>
<CurrentUserContext.Provider
value=
>
{children}
</CurrentUserContext.Provider>
</ThemeContext.Provider>
);
}
function WelcomePanel({ children }) {
const {currentUser} = useContext(CurrentUserContext);
return (
<Panel title="Welcome">
{currentUser !== null ?
<Greeting /> :
<LoginForm />
}
</Panel>
);
}
context와 reducer 결합
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// TasksContext.js
import { createContext, useContext, useReducer } from 'react';
const TasksContext = createContext(null);
const TasksDispatchContext = createContext(null);
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(
tasksReducer,
initialTasks
);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
function tasksReducer(tasks, action) {
switch (action.type) {
case 'added': {
return [...tasks, {
id: action.id,
text: action.text,
done: false
}];
}
case 'changed': {
return tasks.map(t => {
if (t.id === action.task.id) {
return action.task;
} else {
return t;
}
});
}
case 'deleted': {
return tasks.filter(t => t.id !== action.id);
}
default: {
throw Error('Unknown action: ' + action.type);
}
}
}
const initialTasks = [
{ id: 0, text: 'Philosopher’s Path', done: true },
{ id: 1, text: 'Visit the temple', done: false },
{ id: 2, text: 'Drink matcha', done: false }
];
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
// AddTask.js
import { useState, useContext } from 'react';
import { useTasksDispatch } from './TasksContext.js';
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useTasksDispatch();
return (
<>
<input
placeholder="Add task"
value={text}
onChange={e => setText(e.target.value)}
/>
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
</>
);
}
let nextId = 3;
#review