https://www.udemy.com/course/react-next-master/
우리의 app이 다음과 같은 구조로 되어있다고 하자.
|App| (state - todos)
|
---------------------------------
| | (props - onCreate) | ( props - todos, onUpdate, onDelete)
Header TodoEditor TodoList
| (props - onUpdate, onDelete)
TodoItem
여기서 잘보면 App
component에서는 onUpdate
, onDelete
와 같은 props를 TodoList
에게 전달하는데, TodoList
는 onUpdate
, onDelete
props를 직접 사용하지 않고, TodoItem
에 props로 전달해주기만 한다.
만약, 이러한 구조가 깊어진다면 어떻게될까? 너무 복잡해지고 code가 가독성이 안좋아질 것이다. 이를 props drill
현상이라고 한다. promise callback hell과 비슷한 맥락이라고 생각하면 된다.
그래서, 이렇게 props
로 원하는 객체를 쭉쭉 전달하는 것이 아니라, 직접 전달하는 것이 필요해졌고 이를 Context
라고 한다. Context
는 자식 component에게 데이터를 직송으로 보내줄 수 있는 객체인 것이다.
|App| (state - todos) ----> Context(todos, onUpdate, onDelete)
|
-----------------------------------
| | (context - onCreate) | (context -todos)
Header TodoEditor TodoList
| (context - onUpdate, onDelete)
TodoItem
구조가 다음과 같다. props
로 data를 내려주는 것이 아니라, Context
라는 객체를 만들고, 이 객체에 data를 전달해준다음 Context
객체가 다른 react component들에게 data를 직접 주입해주는 것이다. 따라서 props
를 전달하기 위해서 props drilling 현상이 발생하지 않도록 할 수 있다.
사용 방법은 매우 간단하다. 먼저 Context
객체를 만들 jsx
파일을 하나 만들어보도록 하자.
import { createContext } from "react";
export const TodoContext = createContext();
createContext
를 통해서 Context
객체를 만들 수 있는데, 첫번째 인자는 초기값으로 셋팅할 data를 넣어주면 된다. 이제 TodoContext
가 Context
객체가 되는 것이다.
다음으로, TodoContext
를 다른 react component에 주입해주어야 한다. 이를 위해서 다음과 같이 쓸 수 있다.
...
function App() {
...
return (
<div className="App">
<Header/>
<TodoContext.Provider value={{
todos, onCreate, onUpdate, onDelete
}}>
<TodoEditor />
<TodoList />
</TodoContext.Provider>
</div>
)
}
export default App
TodoContext
를 import 한 뒤에 TodoContext.Provider
의 child component로 data를 주입해줄 component를 넣는다. 이때 전달해줄 data는 value
attribute에 넣어주면 된다.
이렇게 설정하면, TodoEditor
와 TodoList
는 TodoContext.Provider
에 의해 Context
객체를 주입받을 수 있다.
다음으로 Context
객체를 통해 data를 주입받아보도록 하자.
import { useRef, useState, useContext } from "react"
import { TodoContext } from "../TodoContext"
...
export default function TodoEditor() {
...
const { onCreate } = useContext(TodoContext)
...
}
useContext
hook을 사용하면 Context의 data를 가져올 수 있는데 TodoContext
를 입력으로 주어야 한다. 이렇게하면 이전에 TodoContext.Provider
의 value로 삽입한 data들을 주입받을 수 있게 되는 것이다.
그런데, Context
를 사용할 때는 몇 가지 주의할 사항들이 있는데, Context
가 불필요한 리렌더링을 유발시킬 수 있다는 것이다. 왜 이러한 문제가 발생하냐면, Context
에 설정된 value가 바뀌면 useContext
를 통해 data를 가져오는 component들은 리렌더링이 발생하기 때문이다.
...
function App() {
...
return (
<div className="App">
<Header/>
<TodoContext.Provider value={{
todos, onCreate, onUpdate, onDelete
}}>
<TodoEditor />
<TodoList />
</TodoContext.Provider>
</div>
)
}
export default App
위의 예제에서 TodoContext.Provider
의 value
로는 todos
, onCreate
, onUpdate
, onDelete
가 있는데, TodoEditor
에서는 useContext
로 onCreate
만 가져왔다. 그러나 todos
, onUpdate
, onDelete
가 바뀌면 TodoEditor
가 해당 value를 사용하지 않아도 리렌더링이 발생한다는 것이다.
정리하자면 Context객체는 value가 변경됨에 따라 다시만들어지고, 해당 Context를 useContext
로 호출하는 component 또한 리렌더링이 발생하게 된다는 것이다.
이는 서로 상관없는 data들이 모두 TodoContext
라는 하나의 context에 몰려있기 때문에 발생한 문제이기 때문에, Context를 분리해주도록 해야한다.
|TodoContext(todos, onCreate, onDelete, onUpdate)|
|
---------------------------------------------
| |
|TodoStateContext(todos)| |TodoDispatchContext(onCreate, onDelete, onUpdate)|
위의 그림은 하나의 TodoContext
를 두 개의 Context
로 나누는데 TodoStateContext
는 todos
에 대한 value
만을 가지도록하고, TodoDispatchContext
는 onCreate
, onDelete
, onUpdate
에 대한 value
만을 가지고도록 하여 분리시킨 것이다. 따라서 TodoStateContext
의 todos
가 변하여 TodoStateContext
가 재생성되어도 TodoStateContext
를 사용하는 측에서는 todos
와 관련이 없기 때문에 재생성되지 않는다.
단, 이미 useCallback
과 React.memo
등으로 최적화를 마친 경우에만 해당한다.
먼저 context객체를 두개로 만들도록 하자.
import { useReducer, useRef, useCallback, useMemo } from 'react'
...
import { TodoStateContext, TodoDispatchContext } from './TodoContext'
function App() {
const [todos, dispatch] = useReducer(reducer, mockData)
...
return (
<div className="App">
<Header/>
<TodoStateContext.Provider value={todos}>
<TodoDispatchContext.Provider value={{
onCreate, onUpdate, onDelete
}}>
<TodoEditor />
<TodoList />
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
</div>
)
}
export default App
그러나 이렇게 만들면 App
component의 state
인 todos
가 변할 때 TodoDispatchContext.Provider
의 value
가 변하게되고 { onCreate, onUpdate, onDelete}
객체를 새로 만들게된다. 이 경우에 불필요한 렌더링이 발생하게 되는 것이다.
불필요한 렌더링이 발생하지 않도록 useMemo
를 사용하여 todos
state가 바뀌어도 리렌더링되지 않도록 하자.
import { createContext } from "react";
export const TodoStateContext = createContext();
export const TodoDispatchContext = createContext()
다음으로 상위 component에 두개의 context객체 provider
를 만들도록 하자.
import { useReducer, useRef, useCallback, useMemo } from 'react'
...
import { TodoStateContext, TodoDispatchContext } from './TodoContext'
function App() {
const [todos, dispatch] = useReducer(reducer, mockData)
...
const memoizedDispatches = useMemo(() => {
return {
onCreate, onUpdate, onDelete
}
}, [])
return (
<div className="App">
<Header/>
<TodoStateContext.Provider value={todos}>
<TodoDispatchContext.Provider value={memoizedDispatches}>
<TodoEditor />
<TodoList />
</TodoDispatchContext.Provider>
</TodoStateContext.Provider>
</div>
)
}
export default App
이렇게 만들면 todos
state가 바뀌어도 TodoDispatchContext.Provider
안의 value
인 memoizedDispatches
가 바뀌지 않아, 리렌더링이 발생하지 않는다.
두 Context
객체를 사용하는 방법은 이전과 동일하다.
import { useRef, useState, useContext } from "react"
import { TodoDispatchContext } from "../TodoContext"
import "./TodoEditor.css"
export default function TodoEditor() {
const [content, setContent] = useState("")
const inputRef = useRef()
const { onCreate } = useContext(TodoDispatchContext)
...
}
useContext
로 Context
객체를 넣어 data를 가져오면 된다.
import { useState, useMemo, useContext } from "react"
import TodoItem from "./Todoitem"
import "./TodoList.css"
import { TodoStateContext } from "../TodoContext"
export default function TodoList() {
const [search, setSearch] = useState("")
const todos = useContext(TodoStateContext)
...
}
이제 todos
를 얻는 Context
와 onCreate
를 얻는 Context
가 달라졌으니, todos
state가 변함에 따라, TodoEditor
component도 리렌더링되지 않는다.