[React] 리액트를 처음부터 배워보자. — 05. Context API
이 글을 남기는 계기는 Dan Ambramov의 블로그 Overreacted를 보며 React.js에 대해 더 깊이 공부할 필요성을 느꼈기 때문이다.
리액트는 기본적으로 데이터를 부모 컴포넌트에서 자식 컴포넌트로 전달하는 단방향 데이터 흐름을 지향한다. 하지만, 컴포넌트를 만들 때 종종 단방향 데이터 흐름보다는 공유되는 데이터 관리가 필요할 때가 있다.
리액트 공식 문서에서는 전체 UI의 테마나 로그인 한 유저 정보 등을 보관할 때 사용한다고 하며 실제로 Context API를 적용한 경우는 현재 SOUNDGYM 앱의 오디오 플레이어 위치를 전역적(Global)으로 관리하기 위해 사용하고 있다.
일반적으로, 전역적(Global) 데이터 관리를 위해 리액트 커뮤니티에서 지원하는 Redux
, MobX
등 별도의 상태 관리 라이브러리를 사용하지만 종종 간단한 작업을 위해 이를 위한 설정 코드를 작성하는 것이 번거로울 때가 있다.
MobX의 경우는 inject, observer 코드가 컴포넌트 코드에 붙어 가독성을 떨어뜨리고, Redux의 경우 하나의 동작을 위해 다수의 Action과 이를 처리하는 코드 작성이 필요하다.
01. Context API 소개
기본적으로 React 라이브러리에서 Context API를 쓸 때 가장 처음 접하는 API는 createContext
이다. 함수명 그대로 Context를 생성하는 함수이며, Context 내부 속성(Property)에는 Provider
, Consumer
가 있다.
[Provider]
Context를 구독하는 컴포넌트들에게 Context의 변화를 알리는 컴포넌트[Consumer]
Context 변화를 구독해 변화시 재 렌더링하는 컴포넌트
Context안의 Provider
와 Consumer
는 React에서 컴포넌트로 분류되며 실제로 createContext
API 구현부를 보면 다음과 같음을 확인할 수 있다.
실제로 Context 안에 컴포넌트 객체가 구현되어 있다는 것을 확인할 수 있다.
특히 Consumer에는 context에 대한 정보를 그대로 반영해 해당 Provider에 대한 데이터를 제공한다는 점이 직관적이었다.
02. Context API 사용법
Context API를 쓰기 위해서는 먼저 createContext
를 통해 Context를 생성해야 한다. 먼저 createAPI를 통해 Context API를 생성해 보자.
이때, defaultValue
에는 Consumer가 Provider를 못 찾을 때 사용할 값을 넣어야 한다. 공식 문서를 대충 읽을 때 이 값이 value가 없을 때의 기본값이라고 생각했었는데 내 착각임을 알 수 있었다.
결국, defaultValue는 Provider가 해당 컴포넌트로 잘 넘어오는지를 파악할 수 있게 해주는 값이지 Provider value에 제공하지 않았을 경우에 기본 값이 아니다.
다음으로 Provider
를 Context를 구독하는 컴포넌트의 최상위 컴포넌트로 선언해야한다. Provider
는 value
props로 받은 값을 하위 컴포넌트에게 전달하는 역할을 합니다.
공식문서에서 Provider 하위에서 Context를 구독하는 모든 컴포넌트는 Provider의 value props가 바뀔 때마다 다시 렌더링 된다고 한다.
이 부분에 대한 렌더링 최적화에 대한 부분은 DongKyun Ko님의 Context Api 어떻게 사용해야 하는가? 에서 잘 설명되어 있다.
마지막으로 해당 Provider
하위에 Consumer
컴포넌트로 감싸진(Wrapping) 컴포넌트르 선언하면 Provider의 value를 구독하는 컴포넌트를 만들 수 있다.
이 때 하위 컴포넌트는 value
를 매개변수로 받고 React.Node
를 반환하는 함수형 컴포넌트로 구현되어야 하는데, 이 부분은 클래스형 컴포넌트에서는 static contextType
로 함수형 컴포넌트에서는 useContext
로 간편화 할 수 있다.
보통 Context API를 이용할 때 모든 하위 컴포넌트가 다시 렌더링 되기 때문에 React.memo
나 shouldComponentUpdate
를 이용해 렌더링 최적화를 한다.
하지만, 이렇게 Consumer
, this.contextType
, useContext
타입으로 Context를 구독하는 컴포넌트들은 상위 컴포넌트에서 렌더링 최적화를 하더라도 Context가 변화하면 자기 자신을 다시 렌더링한다.
03. Context API와 재사용성(Reusability)
Context API를 사용하면 컴포넌트가 Props를 통해 데이터를 받지 않아도 된다는 장점이 있다.
하지만, Context API를 사용하면 Consumer와 Provider와 분리할 수 없기에 재사용성(Reusablility)가 떨어진다는 단점이 있다.
나는 이 부분에 대한 고민을 로버트 C.마틴의 <Clean Architecutre>에서 언급된 불변 컴포넌트(UI 컴포넌트)와 가변 컴포넌트(Consumer 컴포넌트)의 분리 방식으로 접근할 수 있을 것이라 생각했다.
즉, Context API를 사용할 때는 Context API에서 데이터를 받을 컴포넌트의 UI(View)부분을 별도의 컴포넌트로 만들고 Consumer를 Wrapping하는 방식으로 사용하는 것이다.
이 경우 직관적으로 UI 관련된 컴포넌트는 계속해서 재사용 할 수 있다. 이 방식은 매우 기본적인 것 같지만, UI 컴포넌트를 철저하게 분리해야 한다는 것을 알려준다.
즉, UI 컴포넌트를 구상할 때 다음을 고려해 props와 state를 만들어야 한다.
- 중요한 Style 요소(전체 배경색, 각 요소의 글자 크기, 중요 요소의 사이즈)를 props로 받을 수 있도록 해야한다.
- 중요한 컴포넌트의 Event를 props로 받을 수 있도록 해야 한다.
- 컴포넌트에 들어갈 데이터가 구조화가 되어야 한다.
그리고, Context에 의해 영향을 받는 요소는 반드시 로직은 Wrapping된 부분(ConsumerComponent)에 구현해야 한다.
이를 통해 Context API를 사용하더라도 UI의 재사용성을 높일 수 있고 다른 프로젝트에서도 해당 UI를 재사용할 수 있다.
이 부분에 대한 요지는 UI 컴포넌트에 Context를 사용하면 재사용이 어렵다는 것이다. UI 컴포넌트에 Context가 추가되는 순간 그 컴포넌트를 다른 프로젝트에서 재사용 할 수 없다.
결론
이 글을 통해 React 라이브러리에서 공식적으로 지원하는 전역 상태 관리 API Context API
에 대해서 알게 되었다.
Context를 사용하면 중간에 있는 컴포넌트들에게 Props를 넘겨주지 않고 필요한 컴포넌트가 구독을 통해 데이터에 접근할 수 있다.
Context API
를 통해 우리는 단방향 데이터 흐름으로 인해 같은 데이터를 여러 컴포넌트가 공유할 때 발생하는 복잡한 컴포넌트 구성에서 벗어날 수 있다는 것을 알았다.
또한, Context API
를 사용할 때 생기는 재사용성(Reusability) 문제에 대한 접근법에 대해 고민해보았다. 이를 통해 Context API를 전략적으로 어떻게 쓸 것인지에 대해 생각해 볼 수 있었다.
React를 사용하면 “State와 Props를 어떻게 구성할 것인가?”, “컴포넌트 트리를 어떻게 구성할 것인가?” 등에 대한 다양한 고민이 생긴다.
이런 다양한 고민에서 Context API가 하나의 대안이 될 수 있다는 것을 알아두면 좋을 것 같다.
맺음말
개발 공부와 실무를 병행하면 가끔 다음과 같은 생각이 든다. 아마 이 질문은 나만의 문제가 아닐 것이라 생각한다.
“내가 하는 이 방법이 올바른 방법일까?”
이에 대한 답을 찾기위해 어떤 프레임 워크, API, 기능의 활용법에 대해 찾아보면 새삼 소프트웨어를 만드는 방법에 정답은 없다는 생각이 든다.
어떤 방법이든 장단점이 있고, 그 방법이 제공하는 장점으로 인해 제한 사항도 생긴다.
중요한 것은 그 장단점을 파악하고 좀 더 나은 최선의 방법을 찾는 것 같다.
대부분 정답이라 믿고 있는 API의 사용법과 방법론도 대부분 이와 같은 관점에서 탄생한 것이다.
이 같은 관점에서 Context API의 재사용성과 관련한 내 생각보다 더 나은 접근법이 있다면 댓글에 남기거나 그 분야에 대한 글을 남겨주면 많은 사람들에게 도움이 될 것이라 생각한다.