Context

개요

일반적으로 React 에서는 데이터 플로우는 위애서 아래로 (부모 ⇒ 자식) props 를 전달한다. 하지만 앱 내 복잡한 구조 속에서 여러 컴포넌트들이 상태를 공유가 필요하다는 Needs 아래 Context API 가 등장한다!

Context 를 사용하기 전 고려할 것

"context의 주된 용도는 다양한 레벨에 네스팅된 많은 컴포넌트에게 데이터를 전달하는 것입니다. context를 사용하면 컴포넌트를 재사용하기가 어려워지므로 꼭 필요할 때만 쓰세요." - from React official doc
여러 레벨에 걸쳐서 props 를 넘기는 걸 대체하는 데에는 context 보다 때론 컴포넌트 합성이 더 간단한 해결책일 수도 있다!

컴포넌트 합성 example

<Page user={user} avatarSize={avatarSize} /> // ... 그 아래에 ... <PageLayout user={user} avatarSize={avatarSize} /> // ... 그 아래에 ... <NavigationBar user={user} avatarSize={avatarSize} /> // ... 그 아래에 ... <Link href={user.permalink}> <Avatar user={user} size={avatarSize} /> </Link>
JavaScript
복사
function Page(props) { const user = props.user; const userLink = ( <Link href={user.permalink}> <Avatar user={user} size={props.avatarSize} /> </Link> ); return <PageLayout userLink={userLink} />; } // 이제 이렇게 쓸 수 있습니다. <Page user={user} avatarSize={avatarSize} /> // ... 그 아래에 ... <PageLayout userLink={...} /> // ... 그 아래에 ... <NavigationBar userLink={...} /> // ... 그 아래에 ... {props.userLink}
JavaScript
복사
위와 같이 바꾸면 Link 와 Avatar 컴포넌트가 user, avatarSize props 를 사용한다는 걸 알아야 하는 건 가장 위에 있는 Page 뿐이다.
이러한 제어의 역전을 이용하면 넘겨줘야하는 props 의 수는 줄고 최상위 컴포넌트의 제어력은 커지기 때문에 깔끔한 코드를 쓸 수 있는 경우가 많다.
하지만 이 방법이 항상 옳지는 않다. 복잡한 로직을 상위로 옮기면 이 상위 컴포넌트들은 더 난해해지기 마련이고, 하위 컴포넌트들은 필요 이상으로 유연해져야하는 경우가 많아진다.

하지만!

같은 데이터를 트리 안 여러 레벨이 있는 많은 컴포넌트에 전달해야할 경우, 전역 상태관리를 사용하는 것이 좋습니다. 이런 데이터 값이 변할 때마다 모든 하위 컴포넌트에게 알려주는 것이 context 이다.

API

createContext

context 객체를 생성한다. 컴포넌트들은 바로 이렇게 만들어진 context 객체를 subscribe하는 것이다. 컴포넌트들은 트리 상위에서 가장 가까이 있는 짝이 맞는 Provider 로 부터 현재 값을 읽는다.
const MyContext = React.createContext(defaultValue);
JavaScript
복사

Provider

Context 객체에 포함된 React 컴포넌트다! context 를 subscribe하는 컴포넌트들에게 context 의 변화를 알리는 역할을 한다.
value 를 props 로 받아서 이 값을 하위에 있는 컴포넌트들에게 전달한다.
Provider 하위에 또 다른 Provider 를 배치하는 것도 가능하며, 이 경우 하위 Provider 의 값이 우선시된다.
context 를 subscribe 하는 모든 컴포넌트는 Provider 의 value prop 이 변경될 때마다 다시 렌더링된다.
context 값이 바뀌었는지 여부는 Object.is 와 동일한 알고리즘을 사용한다.

주의사항

Provider 의 value 에 객체를 사용할 때, Provider 의 부모가 리렌더링 된다면 하위 컴포넌트도 불필요하게 리렌더링 될 수 있다.
class SomeComponent extends React.Component { render() { return ( <MyContext.Provider value={{something: 'something'}}> <Toolbar /> </MyContext.Provider> ); } } // 위 코드에서는 SomeComponent 가 렌더링 될 때, value 값이 바뀔 때 마다 새로운 객체가 생성되므로 // 하위 컴포넌트도 모두 리렌더링 될 것.
JavaScript
복사

Consumer

context 변화를 구독하는 React Component. 함수 컴포넌트 안에서 context 를 읽고 쓸 수 있다.

이렇게 사용해봅니다

// TodoProvider.tsx class TodoProvider extends React.Component { state: { todos: Todo[]; } = { todos: [], }; public render() { return ( <TodoContext.Provider value={{ todos: this.state.todos, addTodo: this.addTodo, deleteTodo: this.deleteTodo, toggle: this.toggle, }} > {this.props.children} </TodoContext.Provider> ); } private addTodo = (title: string) => { console.log(title); const newTodo: Todo = { id: uuidv1(), title, isDone: false, }; const newTodos = this.state.todos.concat([newTodo]); this.setState({ todos: newTodos }); }; private deleteTodo = (id: string) => { const newTodos = this.state.todos.filter((todo) => todo.id !== id); this.setState({ todos: newTodos }); }; private toggle = (id: string) => { const newTodos = this.state.todos.map((todo) => { if (todo.id === id) { todo.isDone = !todo.isDone; } return todo; }); this.setState({ todos: newTodos }); }; }
JavaScript
복사

Convention 부재

라이브러리, 프레임워크의 장점 중 하나는 best practice 나 convertion, 가이드를 제공한다는 것이다. Context 는 자유도가 높아서 장점이기도 하지만, 협업을 방해하는 요소가 되기도 한다. 소모적인 논쟁, 코드리뷰의 시간 등을 고려하면 결국 생산성을 떨어뜨리게 되기도 한다.

결론

엄밀히 말해서 React Context 는 상태 관리보다는 props drilling 문제점을 해결하기 위한 솔루션으로서 존재한다 (공식 문서에서 state management 라는 용어는 없음).
convention 이 없어서 그런지 '이게 최선인가?' 하는 생각이 자꾸 든다. 뭔가 더 좋은 패턴을 찾으려고 애쓰게 되는 느낌? 높은 자유도는 장점인 것 같가도 하지만 단점인 것 같기도 하다. (최근 들어서는 단점으로 더 많이 느껴진다.)
간단한 전역상태 (유저 ID, 상태, 테마 등)를 다루는데 Context 는 다루기도 쉽고 편할 것 같다. (→ 러닝커브가 낮아보임. 생성(createContext)하고, 값을 넣어주고(Provider), 구독한다(Consumer)). 하지만 복잡해지고 앱의 규모가 커질수록 관리가 힘들 것 같다.