React 재활훈련- 7일차, Context API

0

react

목록 보기
7/11

https://www.udemy.com/course/react-next-master/

Context

우리의 app이 다음과 같은 구조로 되어있다고 하자.

            |App| (state - todos)
              |
    ---------------------------------
    |         | (props - onCreate)  | ( props - todos, onUpdate, onDelete)
 Header    TodoEditor             TodoList
                                    | (props - onUpdate, onDelete)
                                  TodoItem

여기서 잘보면 App component에서는 onUpdate, onDelete와 같은 props를 TodoList에게 전달하는데, TodoListonUpdate, 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파일을 하나 만들어보도록 하자.

  • TodoContext.jsx
import { createContext } from "react";

export const TodoContext = createContext();

createContext를 통해서 Context객체를 만들 수 있는데, 첫번째 인자는 초기값으로 셋팅할 data를 넣어주면 된다. 이제 TodoContextContext 객체가 되는 것이다.

다음으로, 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에 넣어주면 된다.

이렇게 설정하면, TodoEditorTodoListTodoContext.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.Providervalue로는 todos, onCreate, onUpdate, onDelete가 있는데, TodoEditor에서는 useContextonCreate만 가져왔다. 그러나 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로 나누는데 TodoStateContexttodos에 대한 value만을 가지도록하고, TodoDispatchContextonCreate, onDelete, onUpdate에 대한 value만을 가지고도록 하여 분리시킨 것이다. 따라서 TodoStateContexttodos가 변하여 TodoStateContext가 재생성되어도 TodoStateContext를 사용하는 측에서는 todos와 관련이 없기 때문에 재생성되지 않는다.

단, 이미 useCallbackReact.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의 statetodos가 변할 때 TodoDispatchContext.Providervalue가 변하게되고 { 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안의 valuememoizedDispatches가 바뀌지 않아, 리렌더링이 발생하지 않는다.

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)
    ...
}

useContextContext객체를 넣어 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를 얻는 ContextonCreate를 얻는 Context가 달라졌으니, todos state가 변함에 따라, TodoEditor component도 리렌더링되지 않는다.

0개의 댓글