다음과 같은 계층 구조가 있다고 하자.
이때 App 컴포넌트에서 만든 임의의 함수 A를 TodoItem으로 넘기기 위해서는 TodoList를 거쳐서 보낼 수밖에 없다. 이러한 과정은 복잡하고 깊은 계층 구조를 가질수록 끔찍해질 수밖에 없다. 이런 상황을 Props Drilling라고 표현한다.
Props Drilling 상황에서 만약 함수의 이름을 변경하기라도 한다면, 함수가 거쳐가는 모든 컴포넌트를 수정해야 한다.
Context는 이러한 상황을 피하고 자식컴포넌트에게 데이터를 직송으로 보내줄 수 있게 도와준다.
이런 식으로 할 수 있다.
새로운 TodoContext 컴포넌트를 만들고, createContext 메서드를 이용해서 위처럼 하면 Context가 생성되는 것이다. createContext 메서드의 인수로는 Context가 다른 컴포넌트에게 공급할 데이터의 초기값을 넣어줄 수 있다.
그리고 다른 컴포넌트에서 사용할 수 있도록 export 시켜주면 된다.
Context를 사용할 컴포넌트에서 import를 해준다. 이제 TodoContext가 어떤 컴포넌트에 데이터를 공급할지, 그리고 어떤 데이터를 공급할지를 설정해 주면 된다.
1. 어떤 컴포넌트에 공급할지 설정
return문 아래에 <TodoContext.Provider></TodoContext.Provider>를 통해 데이터를 공급할 컴포넌트를 감싸면 된다.
위의 코드에서는 TodoEditor 컴포넌트와 TodoList 컴포넌트뿐만 아니라, 각각의 자손 컴포넌트도 TodoContext로부터 데이터를 공급받을 수 있게 된다.
2. 어떤 데이터를 공급해 줄 것인지
이어서 TodoContext가 어떤 데이터를 공급해 줄 것인지 결정해 주기 위해서 value props로 객체를 전달하면 된다. 이 객체 내부에는 공급할 데이터를 넣어준다.
*Context객체 안에는 여러 기본 프로퍼티들이 들어있다. 이때 Provider라는 프로퍼티에는 Context가 어떤 컴포넌트들에 데이터를 전달할 것인지 결정하는 용도로 사용되는 컴포넌트가 들어있다.
![]() |
![]() |
이로서, 왼쪽의 구조가 오른쪽 처럼 되는 것이다.
한번 더 정리하자면, Provider 컴포넌트 아래에 있는 모든 자손 컴포넌트들은 TodoContext가 공급하는 데이터를 다이렉트로 가져와서 사용할 수 있다.
즉 이제는 TodoList를 거치지 않고 바로 TodoItem으로 데이터를 다이렉트로 꽂을 수 있다.
1,2 완료 이후.
기존의 props를 통해 공급하던 onCreate={onCreate} 같은 것들을 삭제한다. 그리고 Context로부터 onCreate를 불러와서 사용할 수 있도록 설정할 것이다.
TodoEditor 컴포넌트로 이동 후 Context로부터 데이터를 불러오는 리액트 훅인 useContext를 import 한다.
변수를 만들고, useContext를 사용한 뒤, 인수로는 어떤 Context로부터 데이터를 불러올 것인지를 써주면 된다.
앞서 TodoContext를 만들었으므로 인수로 TodoContext를 넣어준다.
console.log를 찍어보면 Context로부터 데이터가 잘 받아와 지는 것을 확인할 수 있다.
이렇게 useContext로 데이터를 잘 받아왔다면, 구조분해할당을 통해 필요한 데이터를 뽑아서 사용하면 끝이다.
Context 적용 후 최적화 풀림
props Drilling을 방지하기 위해 위의 방법을 통해서 Context를 적용하면 기껏 설정해 놨던 최적화가 풀려있는 것을 확인할 수 있다.
<TodoContext.Provider>도 리액트 컴포넌트이기에, value로 받는 데이터(props)가 변경되면 당연히 리렌더링 된다.
컴포넌트가 리렌더링 되면 value를 통해 객체로 공급되는 데이터들도 새롭게 바뀌고, <TodoContext.Provider>에 속한 자손 컴포넌트들도 전부 리렌더 된다.
그런데 React.memo를 사용해서, 직접적인 props 변경이 있지 않는 이상 리렌더를 방지할 수 있게 설정해 놨을 때도 리렌더가 되는 이유가 무엇일까?
Provider에서 value를 통해 전달하는 값이 '객체'이기 때문이다. 객체는 참조 자료형이므로 컴포넌트가 리렌더 될 때마다 참조값이 변경된다. 즉 Provider가 적용된 컴포넌트가 여러 이유로 리렌더 되면, Provider에서 전달하는 객체 또한 재생성되면서 참조 값이 변경되는 것이다.
React.memo가 적용된 컴포넌트에서는 useContext를 호출해서 TodoContext로부터 객체를 불러오는데, 그 객체가 재생성되는 것이다. useContext로부터 불러오는 값이 변경되면 해당 컴포넌트는 React.memo가 적용됐던, 부모가 리렌더 되지 않던, 무조건 리렌더링이 된다.
useContext로부터 어떤 값을 불러오는 상황에서, 그 값이 변경된다는 것은 컴포넌트에 변경이 필요하다는 말이기 때문이다.
즉 Provider가 소속된 컴포넌트가 리렌더 될 때마다 객체의 참조값도 달라지며, Provider의 자손 컴포넌트에 뿌려지는 데이터 또한 변경되는 것이다. 그래서 memo를 통해 최적화를 시켜놔도 리렌더가 되는 것
Context에서 위와 같은 상황 방지하기
만약 Context에서 이를 방지하기 위해서는 변경될 수 있는 값을 제공하는 Context와 변경되지 않는 값을 제공하는 Context를 분리하여, 두 개의 Context를 나눠서 공급하는 것이다.
우선적으로 값이 변경될 수 있는 todos와 값이 변경되지 않는 핸들러들을 분리한다.
이렇게 하면 todos가 필요한 컴포넌트에서는 TodoStateContext를 사용할 수 있고, 핸들러가 필요한 컴포넌트에서는 TodoDispatchContext를 사용할 수 있다.
그렇기에 todos가 변경된다고 해도, TodoStateContext를 사용하는 컴포넌트만 리렌더링 되는 것이다.
단. 이대로 끝내면 위 코드가 작성된 컴포넌트가 리렌더링 될 때마다 {onCreate, onUpdate, onDelete}도 재생성되므로 참조값이 계속 변하게 된다. 즉 여전히 리렌더 될 것이기에 TodoDispatchContext를 분리한 이유가 없어진다.
useMemo를 통해 위처럼 만들어주고, 핸들러들이 재생성되는 것을 막으면, 최종적으로 불필요한 리렌더링을 막을 준비는 끝난 것이다.
<주의점>
TodoStateContext.Provider에서 value로 객체가 아니라 todos를 그대로 보냈다.
그렇기에 위처럼 구조분해 할당으로 받으면 당연히 오류가 생긴다.
주의하자
'프로그래밍 언어 > React 기초' 카테고리의 다른 글
리액트 드래그 앤 드롭(DND) (0) | 2023.11.27 |
---|---|
리액트의 페이지 라우팅 (0) | 2023.11.26 |
리액트 최적화(useMemo,React.memo,useCallback) (0) | 2023.11.15 |
리액트 useReducer (0) | 2023.11.15 |
리액트 todoList 체크박스 수정, 삭제 (0) | 2023.11.13 |