React
Context API
사용하다보니 메모
Context를 사용하다보면 하나의 상태값을 여러 컴포넌트에서 사용하기 때문에, 상태값을 사용하고 있는 여러 컴포넌트에서 불필요한 리렌더가 발생할 수 있다고 한다.- 예를 들어,
example이라는 값을 const {example} = useContext(ExampleContext)로 두 개의 컴포넌트에서 사용하고 있다. A 컴포넌트에서 값을 변경했어도, 이 값을 사용하고 있는 B 컴포넌트에서도 리렌더링이 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| type DefaultValue = {
example: string;
setExample: React.Dispatch<React.SetStateAction<string>>;
};
const AppContext = createContext<DefaultValue>({
example: "",
setExample: () => {},
});
function App() {
const [example, setExample] = useState("");
return (
<AppContext.Provider value=>
<ExampleHeader />
<ExampleBody />
</AppContext.Provider>
);
}
|
- 위와 같은 코드에서
ExampleBody 컴포넌트에서 example값을 변경한다고 가정하면, 최상위 컴포넌트에 포함돼있는 ExampleHeader에서도 리렌더링이 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| function ExampleHeader(){
console.log(1);
return(
...
)
};
function ExampleBody(){
const {setExample} = useContext(AppContext);
// example을 변경하는 핸들러
function changeExampleHandler(e:React.EventHandler){
setExample(e.target.value);
}
return (
// example 변경
)
};
|
example값이 변경될 때마다 console에 1이 찍히며, 불필요한 리렌더링을 해결할 필요가 있었다.
방법 1. React.memo 사용하기
참고자료 React.memo 참고자료 - 리액트 memo를 사용하기 전에
- 컴포넌트를 렌더링 할 때 사용하는
React.memo를 사용한다. memo는 컴포넌트를 렌더링한 뒤, 이전 렌더링 결과와 다르면 업데이트를 하는데, 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
| type ExampleType = {
example: string,
setExample: React.Dispatch<React.SetStateAction<string>>,
};
const ExampleContext = createContext({
example: "",
setExample: () => {},
});
const App = () => {
const [example, setExample] = useState();
return (
<Example.Provider value=>
<ChildOne />
<ChildTwo />
</Example.Provider>
);
};
const ChildOne = () => {
console.log("ChildOne component render");
return null;
};
const ChildTwo = () => {
console.log("ChildTwo component render");
const { example, setExample } = useContext(ExampleContext);
return <button onClick={() => setExample("changed")}>example 변경</button>;
};
|
- 위와 같은 코드가 있다면 콘솔에 컴포넌트 1,2에 해당하는 콘솔 메시지가 전부 찍혀있을 것이다.
ChildTwo에서 버튼을 사용하여 example값을 변경할 때, 값을 사용하고 있지 않은 ChildOne 컴포넌트에 대한 불필요한 리렌더링이 발생하기 때문에 콘솔 메시지가 찍히는 것을 볼 수 있다.- 이는 컴포넌트의 상태가 변경되면 해당 컴포넌트의 하위 컴포넌트도 리렌더링 되는 컴포넌트의 특성때문이다.
ChildOne은 example의 상태값을 전혀 사용하고 있지 않지만, App컴포넌트의 하위 컴포넌트에 속하기 때문에, 설령 값을 사용하지 않고, ChildTwo컴포넌트에서 값을 변경하더라도 ChildOne컴포넌트에 대한 불필요한 리렌더링이 발생한다.- 이를 방지하기 위해
React.memo를 사용하여 컴포넌트의 리렌더링을 방지할 수 있다.
const ChildOne = React.memo(() => {
console.log("ChildOne component render")
return null;
});
ChildOne 컴포넌트를 React.memo로 감싸 리렌더링을 방지할 수 있는데, React.memo는 순수함수를 상속받는 것과 같이 동작한다고 한다.- 처음 렌더링 될 때 콘솔에
"ChildOne component render"가 찍히고, ChildTwo에서 상태값을 변경하더라도 ChildOne 컴포넌트에 대한 변경점이 없기 때문에 리렌더링이 발생하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const ChildTwo = () => {
const { example, setExample } = useContext(ExampleContext);
console.log("ChildTwo component render");
return (
<React.Fragment>
<ChildThree />
<button onClick={() => setExample("changed")}>example 변경</button>
</React.Fragment>
);
};
const ChildThree = () => {
console.log("ChildThree component render");
return null;
};
|
- 만약
ChildTwo 컴포넌트의 하위 컴포는트로 ChildThree 컴포넌트가 있다면 어떨까. ChildTwo에서 상태값을 변경할 때, 상태값을 사용하고 있지 않은 ChildThree 컴포넌트에 대한 리렌더링도 똑같이 발생한다.- 이는 위에서 설명한 컴포넌트의 특성때문에
ChildTwo 컴포넌트가 리렌더링 되면서 하위 컴포넌트들 또한 리렌더링 시키기 때문이다.
1
2
3
| const ChildThree = React.memo(() => {
...
})
|
- 똑같이
React.memo를 사용하여 리렌더링을 방지할 수 있지만, 매 컴포넌트마다 React.memo를 기본값으로 작성해주는 것도 번거로운 일이다. - 리렌더링이 발생하는 컴포넌트에 대한 정리를 통해서 이러한 과정을 최소화 할 수 있을 거라 생각하지만, 더 좋은 방법이 있는지에 대해서도 생각해야겠다.
방법 2. 상태 끌어올리기
참고자료
Context를 분리하고 이를 최상위 컴포넌트인 App에서 관리하여 전체가 리렌더링 되는 것을 방지한다.- 수정 전의 예시와 수정 후의 예시는, 수정 전의 코드는
Context에 관한 상태값을 App컴포넌트에서 관리한다. - 그렇기에 수정 사항이 발생하면 하위 컴포넌트들이 전부 리렌더링된다.
- 이진 트리에서 꼭대기의 시작점부터 변경되는 것이라고 생각하면 된다.
- 수정 후의 예시는,
App에서 ChildOne, ChildTwo를 렌더링하여 ContextProvider로 넘겨주는 역할을 하기 때문에 App의 리렌더링이 발생하지 않는 이상 ContextProvider가 리렌더링되어도 하위 컴포넌트들의 변경사항은 발생하지 않는다.
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
| // 기존 코드
const AppContext = createContext();
const App = () => {
const [str, setStr] = useState();
console.log("App render")
return (
<AppContext.Provider value=>
<ChildOne/>
<ChildTwo/>
</AppContext.Provider>
)
};
// ChildOne
const ChildOne = () => {
console.log("ChildOne render");
return null
}
// ChildTwo
const ChildTwo = () => {
const {str, setStr} = useContext(AppContext);
console.log("ChildTwo render");
return (
<div>
<span>current Str: {str}</span>
<button onClick={() => setStr(...)}>Random Str</button>
</div>
)
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 수정 코드
const AppContext = createContext();
const ContextProvider = ({ children }) => {
const [str, setStr] = useState();
console.log("Provider render");
return (
<AppContext.Provider value=>
{children}
</AppContext.Provider>
);
};
const App = () => {
console.log("App render");
return (
<ContextProvider>
<ChildOne />
<ChildTwo />
</ContextProvider>
);
};
|
방법 3. Context 분리하여 사용하기
참고자료
- 별도의
Context와 Provider을 만들어 분리하고, App 컴포넌트에서 상태끌어올리기를 사용하는 방식을 함께 사용한다. - 빠르게 예시를 봐보자.
- 아래와 같이
Context, Provider를 분리하여 작성하고, App 컴포넌트에서 reduce함수를 사용해 사용할 Context를 모두 가져와주고, 렌더링 될 App의 하위 컴포넌트들을 전부 가져온다. - 이렇게
Context를 분리하여 여러 개를 사용하면, 사용하는 컴포넌트와 Provider에 대한 렌더링만 발생하게 되어 불필요한 렌더링을 방지할 수 있다.
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
| // StrProvider.js
export const StrContext = createContext("set Str");
export const StrProvider = ({children}) => {
const [str, setStr] = useState("");
console.log("StrProvider render")
return (
<StrContext.Provider value=>
{children}
</StrContext.Provider>
)
};
// NumProvider.js
export const NumContext = createContext(0);
export const NumProvider = ({children}) => {
const [num, setNum] = useState(0);
console.log("NumProvider render");
return (
<NumContext.Provider value=>
{children}
</NumContext.Provider>
)
}
// App.js
const AppProvider = ({ contexts, children }) =>
contexts.reduce(
(prev, context) =>
React.createElement(context, {
children: prev,
}),
children
);
const App = () => {
console.log("App render");
return (
<AppProvider contexts={[StrProvider, NumProvider]}>
<ChildOne />
<ChildTwo />
</AppProvider>
)
};
// ChildOne
const ChildOne = () => {
const {str, setStr} = useContext(StrContext);
console.log("ChildOne render")
return (
<div>
<span>current Str: {str}</span>
<button onClick={() => setStr(...)}>random Str</button>
</div>
)
}
// ChildTwo
const ChildTwo = () => {
const {num, setNum} = useContext(NumContext);
console.log("ChildTwo render");
return (
<div>
<span>current Num: {num}</span>
<button onClick={() => setNum(...)}>random Num</button>
</div>
)
}
|
방법 4. 값을 별도의 객체로 관리 (PASS)
참고자료
- 객체를 통째로 상태값으로 관리하게 되면, 상태 객체가 변경되지 않는 경우에는 하위의 컴포넌트가 불필요하게 리렌더링 되지 않는다고 한다.
- 잘 이해를 못 하겠다…상태 객체는 어차피 변경될 여지로 만들어 두는 게 아닌가…? 어떻게 하면 리렌더링이 일어나지 않을까 여러 방법으로 시도해보았지만, 위에서 설명하는 리렌더링이 안 일어나는 방법은 뭔지를 모르겠다.
- 예시 1번 코드를 예시 2번 코드로 바꿔서 얻는 이점이 뭘까…모르겠다.
예시 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const UserContext = createContext({ username: "unknown", age: 0 });
export default function App() {
const [username, setUsername] = useState("");
const [age, setAge] = useState(0);
return (
<div>
<UserContext.Provider value={(username, age)}>
<Profile />
</UserContext.Provider>
</div>
);
}
|
예시 2
1
2
3
4
5
6
7
8
9
10
11
12
13
| const UserContext = createContext({ username: "unknown", age: 0 });
export default function App() {
const [user, setuser] = useState({ username: "horong", age: 23 });
return (
<div>
<UserContext.Provider value={user}>
<Profile />
</UserContext.Provider>
</div>
);
}
|
방법 5. useMemo 사용하기 (PASS)
참고자료
useMemo를 활용하여 리렌더링을 방지하는 방법에 대해 설명하고 있다.- 직접 사용하여 실험해보고 있지만 리렌더링은 똑같이 발생한다.
- 의문이 들었던 것은
useMemo로 값을 기억할 수 있지만, Context에서 사용하는 값이 변경될 때면 메모이제이션이 새로 발생하며 리렌더링이 당연히 일어나게 되는 것 아닐까? 이 방법으로 어떻게 리렌더링을 최적화하는 것인지 의문이다.