Context

ANN·2026년 1월 31일

OneBiteReact

목록 보기
8/8
post-thumbnail

📌 Context

React Context컴포넌트 간의 데이터를 전달하는 또 다른 방법이고,
기존의 Props가 가지고 있던 단점을 해결할 수 있음

☑️ Props의 단점

Props는 부모 ➡️ 자식으로만 데이터를 전달할 수 있음


  • 한 단계 아래로 전달하는 건 문제 되지 않음

하지만,
계층 구조가 이렇게 두 단계 이상 깊어지면,
App 컴포넌트에서 B 컴포넌트에게 데이터를 전달할 때
props를 이용하게 되면 다이렉트로 전달 불가능

왜냐하면, props는 부모에서 자식으로만 데이터를 전달할 수 있기 때문에,
자식 A라는 컴포넌트가 중간 다리 역할을 해야 함
➡️ 먼저 자식 A에게 데이터를 전달하고, 그 다음 자식 A가 자식 B에게 데이터를 연달아 전달하는 방식으로 props를 이용


To-Do 아이템을 만들 때,
계층 구조상 아이템 수정이나 삭제 기능은
App 컴포넌트가 가지고 있는 onUpdate나 onDelete 같은 함수는
건너건너 TodoItem 컴포넌트에게 전달했어야 했음

현재 App 컴포넌트와 TodoItem 컴포넌트는 부모-자식 관계가 아니기 때문에
중간 다리로 List 컴포넌트를 거쳐서
데이터를 이중으로 전달해야 했음


위와 같이,
props를 이용하면 데이터를 중간 다리를 거쳐 전달하는 경우가 많은데
좋은 방법은 당연히 아님

서비스가 커지고,
더 많은 컴포넌트가 그 사이에 존재하면
그때그때 props를 추가해야 하고,
추후, props명이 변경될 때면 일일히 이름을 변경해야 함

리액트에서는 이 상황을
props가 드릴처럼 땅을 파고 내려가는 것 같다고 해서
Props Drilling이라고 함


이러한 문제들을 해결하기 위해
React Context를 사용해보고자 함


Context는,
데이터를 보관하는 일종의 데이터 보관소 역할을 하는 객체

컨텍스트를 새롭게 생성 후,
App 컴포넌트에서 원래 자식 컴포넌트에게 전달되던 함수를 컨텍스트에 보관하면,
이제부터 props를 이용하지 않고
직통으로 컨텍스트를 통해 필요한 데이터 공급 가능

그리고 이러한 이러한 컨텍스트는 여러 개 만들 수 있음

왼쪽의 L 자식 컴포넌트는 A 컨텍스트의 데이터만 공급받을 수 있고,
오른쪽의 R 자식 컴포넌트는 B 컨텍스트의 데이터만 공급받을 수 있도록 설정 가능


📌 Context 사용하기

App 컴포넌트에서 선언한,
onUpdate와, onDelete는 TodoList 컴포넌트의 props로 전달되어
TodoItem 컴포넌트까지 props로 전달되어야 함
➡️ Props Drilling


☑️ Context 생성

새로운 컨텍스트 객체 생성

보통은 컴포넌트 외부에 컨텍스트를 선언
만약, 안에 선언하면 App 컴포넌트가 리렌더링되면 컨텍스트도 새로 생성하게 됨

근데 이 컨텍스트는 데이터를 하위에 있는 컴포넌트에 공급할 뿐이고,
앱 컴포넌트가 리렌더링될 때마다 다시 생성될 필요가 없음

따라서 특수한 경우가 아니면,
컨텍스트의 생성은 컴포넌트 외부에 선언함


콘솔에 출력해보면 위와 같은 결과가 나옴
➡️ 여러 개의 프로퍼티를 가지는 객체

다양한 정보가 컨텍스트에 동작하고,
대부분은 내부 동작용

우리는

위, Provider 공급자, 제공자만 보면 됨
➡️ 컨텍스트가 공급할 데이터를 설정하거나, 컨텍스트의 데이터를 공급받을 컴포넌트를 설정하기 위해 사용

사실은 컨텍스트도 컴포넌트임


☑️ 데이터 공급

위와 같이 리턴문에 사용
이제 이 사이에 있는 컴포넌트는,
TodoContext의 데이터를 공급받을 수 있음

공급할 데이터는
컴포넌트에 value라는 prop로 전달


onCreate, todos, onUpdate, onDelete 다 컨텍스트로 공급할 거니까

이렇게 value를 통해서 전달


이렇게 Provider 컴포넌트의 자식, 또는 자손 컴포넌트는 value props를 통해서
우리가 설정한 컨텍스트가 공급하는 값들을 다이렉트로 꺼내와서 언제든지 사용할 수 있는 상태가 되도록

  • TodoContext란 컨텍스트 생성
  • 해당 컨텍스트의 Provider를 Editor 컴포넌트와 List 컴포넌트의 부모 컴포넌트로 둠

그리고 props로 함수와 state를 객체로 묶어 전달

이제 Provider 아래 있는 컴포넌트는,
TodoContext에 저장된 데이터를 바로 다이렉트로 공급 받을 수 있음

이제부터 Editor 컴포넌트는 TodoContext로부터 onCreate 함수를 불러와 사용이 가능하고,
List 컴포넌트는 todos state를,
TodoItem 컴포넌트는 함수를
다이렉트로 불러올 수 있음

컨텍스트 Provider 컴포넌트 안에 있는 컴포넌트는,
해당 컨텍스트의 데이터를 공급 받아 이용이 가능

브라우저의 컴포넌트에서도 확인 가능
설정한 대로 Editor와 List를 감싸고 있음


☑️ 데이터 꺼내기

이제 데이터를 꺼내서 써보자

Editor가 받던 Props를 제거하고,
컨텍스트에서 해당 데이터를 꺼내올 것임


useContext라는 훅을 사용하고,
인수로는 불러오고자 하는 컨텍스트를 넣음

(당연한 얘기지만(??)) 해당 컨텍스트는 export를 해야 함)


아무튼 이제 useContext라는 리액트 훅은
인수로 전달한 컨텍스트로부터 공급된 데이터를 반환

그렇기 때문에 데이터라는 변수에 TodoContext가 제공한 값들이 들어있음

이제 값을 잘 불러왔는지,
콘솔을 통해 확인 한 번 해보면,

App 컴포넌트에서 Provider 컴포넌트에게 props로 공급했던 함수가
Editor 컴포넌트에게 잘 공급됨


Editor 컴포넌트에서는 onCreate만 사용하면 되니까,
구조분해 할당을 이용해 불러옴

이제 props가 아닌 다이렉트로 데이터를 공급 완료!


  • List 컴포넌트도 props를 제거
  • useContext를 통해 컨텍스트에서 데이터를 받아옴
    (onUpdate, onDelete는 List 컴포넌트에서 사용하지 않을 것이므로 받아오지 않음)

TodoItem에서 해당 함수들을 직접 받아올 것이므로,
내려주는 props에서도 제거


TodoItem에서도 onUpdate, onDelete 함수를 props로 받아오지 않고,
useContext를 통해 구조분해 할당으로 받아오기


그런데 useCallback과 memo로 적용한
TodoItem들의 최적화가 풀려서
다른 아이템들도 리렌더링이 됨

🫠


📌 Context 분리하기

컨텍스트를 적용하니,
이전에 적용해 두었던 최적화가 풀리는 문제를 확인

프로바이더 컴포넌트도 엄연히 리액트의 컴포넌트이기 때문에,
App 컴포넌트로부터 value props로
제공받는 todos(state), onCreate, onUpdate, onDelete를 감싸고 있는 객체가 바뀌게 되면
즉, 쉽게 말하면 props가 바뀌게 되면 리렌더링 발생

근데, 이 객체가 왜 바뀜?


새로운 아이템을 추가/수정/삭제했을 때
todos state가 바뀌어서,
다시 객체를 생성해서 넘겨주게 됨

➡️ 그래서 프로바이더 컴포넌트도 리렌더링이 발생

따라서,
Editor, List, TodoItem 같은 하위 컴포넌트도
부모 컴포넌트가 리렌더링 되었기에,
자식 컴포넌트도 함께 리렌더링이 발생


이전에, TodoItem 컴포넌트에 최적화를 적용하기 위해서
memo 메서드를 써서 자신이 받는 props가 바뀌지 않으면
아예 리렌더링을 발생시키지 않도록 설정해둔 적이 있음

그런데 왜 리렌더링이 발생?


todo 아이템을 추가/수정/삭제할 경우,
todos state가 바뀌어서,
App 컴포넌트가 리렌더링이 되고,
프로바이더 컴포넌트에게 value props로 전달하는 객체 자체가 다시 생성됨

그렇기 때문에 TodoItem 컴포넌트에서,
useContext를 호출해서
TodoContext로부터 불러온 이 객체 자체가 다시 생성되어서
결국 TodoItem도 리렌더링이 발생한다는 것

메모를 적용했더라도, useContext로 불러온 값이 변경되면,
이것은 props가 변경된 것과 동일하게 리렌더링을 발생
시킴

☑️ 문제 해결

현재 하나인 TodoContext를 두 개의 컨텍스트로 분리

  • todos처럼 state이므로 변경될 수 있는 값은 TodoStateContext를 만들어 공급
  • 반면에, 변경되지 않는 값들은(예전에 useCallback으로 묶어둔 적도 있는)
    또 컨텍스트를 만들어 분리해서 공급

➡️ todos state가 변경되어서 TodoStateContext가 공급하는 값이 바뀌어도
함수는 바뀌지 않기 때문에,
TodoDispatchContext가 공급하는 값은 변경되지 않음


두 개의 프로바이더 생성

프로바이더 생성


todos state는 TodoStateContext의 프로바이더에게만 value props로 공급해주고,

나머지 onCreate, onUpdate, onDelete 같은 변치 않을 함수는
TodoDispatchContext의 프로바이더에게만 value props로 공급해주면 됨

각 컴포넌트에서 데이터를 받아옴

TodoEditor는 새로운 Todo 아이템을 추가해야 하니까,
TodoDispatchContext로부터 onCreate를 꺼내와서 쓰면 됨


TodoITem 컴포넌트도
TodoDispatchContext로부터 onUpdate, onDelete를 꺼내와서 쓰면 됨


마지막으로 List 컴포넌트는
todos state를 가져와야 하니까,
TodoStateContext로부터 todos를 꺼내와서 씀


동작 매커니즘

만약 todos state가 갑자기 업데이트(수정/삭제/생성)되어도,

다른 컴포넌트는 리렌더링되지 않고,
TodoList 컴포넌트만 리렌더링 될 것


원래라면
부모 컴포넌트가 리렌더링되면 리스트와 에디터, 아이템이 모두 렌더링되는 게 맞지만

이렇게 컨텍스트도 분리하고, 메모를 적용해놓았기 때문에
TodoItem에는 리렌더링이 이때는 발생하지 않게 되는 것

➡️ 이 구조대로 코드 변경

코드에 적용

기존 컨텍스트를 두 개로 분리


각 컨텍스트는 자신이 관리할 데이터를 가지고 Provider 컴포넌트로 각 컴포넌트를 포함
(아까 위 흐름도를 참고...)


App 컴포넌트의 todos state가 변경되어 리렌더링이 되면 객체가 다시 생성될 건데,
그외는 useMemo로 객체를 다시 생성하지 않도록 하자

한번 마운트된 이후로는 리렌더링되어도 다시 생성하지 않도록
deps는 빈 배열로 둠

변경되지 않을 것들은 useMemo의 콜백함수 안에서 리턴

이제 이 memoizedDispatch 변수를 전달하면 됨
➡️ TodoDispatchContext가 공급하는 값은 어떠한 상황에서도 변경되지 않을 것


Editor 컴포넌트는,
onCreate를 TodoDispatchContext로부터 함수를 받음


List 컴포넌트에선,
todo를 value로 저장하면 구조 분해가 아니라 바로 받아올 수 있음
(지금은 객체로 저장했음...)


TodoItem 컴포넌트에선,
onUpdate, onDelete 함수를 구조분해 할당으로 받아옴


최적화 적용 확인


☑️ 정리(?)

Props drilling을 방지하고,
해당 컨텍스트를 통해 넘기는 객체 때문에 리렌더링되는 걸 방지하기 위해...
컨텍스트를 분리하기도 하고,
useMemo로 다시 감싸줌

그런데...

이 컨텍스트는 전역 상태 관리 용도로 사용하기엔 좋지 않다고 함
...

따라서 React의 상태 관리에 대해 더 공부해 보자

0개의 댓글