[React] Context
React Context
컴포넌트 간의 데이터를 전달하는 또 다른 방법, 기존의 Props가 가지고 있던 단점을 해결할 수 있다.
Props는 어떤 단점이 있을까? 🧐
Props Drilling
Props: 부모 → 자식으로만 데이터 전달 가능
데이터를 컴포넌트의 이 계층 구조 상에서 한 단계 아래로 전달하는 것은 문제가 없었다.
그 이유는 앱 컴포넌트와 child 컴포넌트가 현재 부모와 자식 관계를 가지조 있기 때문이다.
(데이터를 바로 전달 가능했었음)
그렇다면 컴포넌트의 계층 구조가 두 단계 아래로 전달해야할 때는?
App 컴포넌트에서 ChildB 컴포넌트에게 데이터를 전달할때 Props를 이용하면
다이렌트로 전달 불가능이다. (Props는 부 → 자만 가능했기에)
이를 해결하기 위해서는 App -데이터 전달→ ChildA -데이터 전달→ ChildB
서비스의 규모가 작아서 위에와 같은 방법에 문제가 없었지만 서비스의 규모가 커지면
많은 컴포넌트를 거쳐서 데이터를 전달해야된다. Props의 이름이 바뀌게 되면 모든 컴포넌트의 Props이름을 찾아 수작업으로 바꿔야되는 불편사항이 생긴다
이런 상황을 Pops Drilling 이라고 부른다.
이를 해결하기 위한 방법은 React Context이다. 😄
Context는 데이터 보관소(객체)다. Context를 통해 필요한 데이터를 바로 보낼 수 있다. 이를 통해 Props Drilling 문제 해결 가능!
A Context, B Context에서 나눠받는게 가능하다!
Context 사용하기
import {
useState,
useRef,
useReducer,
useCallback,
createContext,
} from 'react';
...
const TodosContext = createContext();
function App() {
...
}
context는 컴포넌트 외부에 선언한다. 그 이유는 App 컴포넌트 안쪽에 context 객체를 생성할 경우 app컴포넌트가 리렌더링이 될 때마다 계속해서 context로 선언한 라인이 실행하기 때문에 생성한 context가 생성됨을 방지하기 위해 외부에 선언한다. (특수한 경우가 아니면 다시 생성될 경우는 없다.)
Provider(공급자, 제공자)
👉🏻 Context가 공급할 데이터를 설정하거나,
Context의 데이터를 공급받을 컴포넌트들을 설정하기 위해서 사용하는 props이다.
Provider는 컴포넌트이다. 😲
<TodoContext.Provider/>
이렇게 코드를 작성해 렌더링도 쌉가능
Provider 컴포넌트 (안에) 아래에 있는 모든 컴포넌트들은 TodoContext의 데이터를 공급받을 수 있다.
그렇다면 공급할 데이터는 어떻게 설정할까? 🤷🏻♀️
<TodoContext.Provider>의 value 속성은 하위 컴포넌트들이 사용할 수 있는 데이터(상태, 함수 등)를 담는 객체를 넘겨주면 된다.
TodoContext 컴포넌트를 생성하고 해당 context의 Provider를 Editor 컴포넌트와 List 컴포넌의 부모 컴포넌트로서 설정해두면 된다. ValueProps로 App 컴포넌트 안에 있는 TodoState그리고 OnCreate, OnUpdata, OnDelete 함수를 객체로 묶어 전달했었다.
전달한게 다이렉트로 바~로 공급받을 수 있게 된다 .
vscode에서 이 editor 컴포넌트로 부터 이 context로부터 제공받는 값을 바꿔볼 수 잇도록 해보겠다.
onCreate 함수는 이제 Context를 통해 하위 컴포넌트에 공급되기 때문에, 더 이상 props로 직접 전달할 필요가 없어져서 제거할 수 있다.
useContext()의 인수로는 데이터를 불러오고자하는 context를 직접 넣으면 된다.
파일을 내보낼 수 있게 export를 해준다.
useContext의 인수로 불러온다.
이렇게 작성하면 된다. 다른 컴포넌트들 역시 이렇게 작성하면 된다.
최종적으로 이렇게 된다.
Context 분리하기 🪴
TodoItem컴포넌트에서 useContext(TodoContext)를 통해 onUpdate, onDelete함수를 받아오고 있다. 이때 App 컴포넌트안의 TodoXontext.Provider는 아래와 같이 value를 객체 형태로 전달하고 있다.
<TodoContext.Provider value={{ todos, onCreate, onUpdate, onDelete }}>
❗️문제의 핵심 🚨
이렇게 value={{ ... }} 안에 객체 리터럴을 직접 정의하면, React는 매 렌더링마다 새로운 객체로 인식한다.
즉, value가 항상 새로운 참조가 되어버리는 것!
결과적으로 TodoItem은 매번 리렌더링 된다. 😭 (심지어 props가 변하지도 않았는데 말이당..) 🔓 최적화가 풀렸다.
그러면 우리는 이유를 생각해 볼 수 있다.
왜?
React는 객체의 내부 값이 같아도, 객체의 주소(참조)가 다르면 변화된 값으로 간주된다.
value={{ onUpdate, onDelete }} // 매번 새로운 객체 생성됨
이게 바로 useContext를 쓴 컴포넌트가 불필요하게 계속 리렌더링되는 이유이다.
해결 방법은?
ToDoContext를 두개의 context로 분리해 해결할 수 있다.(todos는 state임) 변경될 수 있는 값은 ToDoState라는 새 context를 만들어 공급하고 반대로 변경되지 않는 함수들은 TodoDispatchContext라는 새 context를 만들어 공급한다.
TodoStateContext가 바뀌더라고 TodoDispatchContext는 바뀌지 않는다.
계층구조 표현
App 컴포넌트에서 변하는 todos 함수는 TodoStateContext.Provider, 변하지 않는 onCreate, onUpdate, onDelete함수들은 TodoDispatchContext.Provider로 ValueProps로 넣어주면 된다.
🟥 TodoStateContext.Provider
- todos 상태(할 일 목록)을 전역적으로 공급하는 Context.
- 이 Provider 안에 있는 컴포넌트는 todos 상태를 사용할 수 있다.
🟦 TodoDispatchContext.Provider
- onCreate, onUpdate, onDelete 같은 상태 변경 함수들을 공급하는 Context.
- 이 Provider 안에 있는 컴포넌트는 dispatch 또는 상태 수정 함수를 사용할 수 있다.
- App 컴포넌트
- 루트 컴포넌트로서 TodoStateContext.Provider와 TodoDispatchContext.Provider를 자식들에게 공급함.
- Header
- onCreate, onUpdate, onDelete를 사용 → TodoDispatchContext만 필요.
- Editor
- onCreate 사용 → 상태 수정 필요 → TodoDispatchContext 필요.
- List
- todos 데이터가 필요하므로 TodoStateContext 필요.
- 내부의 TodoItem이 onUpdate, onDelete를 쓰므로 TodoDispatchContext도 필요.
Todo List만 리렌더링이 되는 것이다.
const meomizedDispatch = useMemo(() => {
return { onCreate, onUpdate, onDelete };
}, []);
1. useMemo를 사용하는 이유
- 컴포넌트가 리렌더링될 때 같은 객체가 계속 새로 생성되면 불필요한 렌더링이 발생한다.
- useMemo는 불변 객체를 기억해서 다시 생성되지 않도록 한다.
- 특히 DispatchContext처럼 변하지 않는 값을 공급하는 컨텍스트는 useMemo로 감싸줘야 효과적이다.
2. 구조분해 할당을 안 쓰는 이유
- 예전에는 ToDoItem 컴포넌트에서 DispatchContext에서 여러 값을 구조분해 할당으로 꺼내 썼다.
- 예: onCreate, onUpdate, onDelete
- 이 경우는 각 함수들을 개별로 꺼내 쓰기 위해 구조분해 한 것다.
3. 지금은 구조분해 안 쓰는 이유
- ToDoStateContext에서는 그냥 상태 객체(toDos)만을 넘겼다.
- 구조분해가 아닌, 변수 그대로 넘겨서 컴포넌트에서 사용한다.
- 객체가 아닌 단일 배열 형태니까 구조분해가 필요 없다.
결론
- 불변 값 공급은 useMemo로 감싸야 효율적
- 구조분해 할당은 여러 개별 함수가 있을 때만 사용한다.
- 지금은 배열만 전달하므로 구조분해 없이 바로 사용하면 된다.