React
에서는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 때 props
객체를 이용해서 데이터를 전달한다.
하지만 데이터를 필요로 하는 자식 컴포넌트의 개수가 많다면 같은 코드를 반복해서 입력해야 하고, 중첩 구조를 가질 때 중간에 있는 컴포넌트들은 해당 데이터가 필요하지 않아도 의무적으로 자식 컴포넌트에게 데이터를 전달해야 한다.
이럴 때 컨텍스트(Context)를 활용하면 좀 더 효율적으로 처리할 수 있다.
간단한 예제를 통해 컨텍스트가 필요한 경우부터 알아보자.
App
컴포넌트에서 props
객체를 이용해 유저 정보를 자식 컴포넌트에게 전달하고, UserViewer
컴포넌트에서 해당 데이터를 사용해 렌더링 하는 간단한 예제다.
UserWrap
컴포넌트를 보면 유저 정보가 필요 없기 때문에 다시 자식 컴포넌트로 props
를 전달하는 것을 볼 수 있다.
이렇게 데이터를 사용하려는 컴포넌트가 아래에 있으면 중간에 있는 컴포넌트는 의미 없이 데이터 전달만 하게 되고, 컴포넌트 트리가 복잡할수록 더욱 문제가 생긴다.
그럼 컨텍스트를 이용한 코드로 고치기 전에, 어떻게 사용해야 하는지 API
부터 확인하고 넘어가자.
React.createContext(defaultValue)
ex)const ColorContext = React.createContext('red')
컨텍스트 객체를 생성하는 메서드다.
생성된 컨텍스트 객체는 Provider
와 Consumer
컴포넌트를 가지며, 이를 이용해 데이터의 변경 및 감지가 가능하다.
인자로 전달되는 defaultValue
의 경우, 컨텍스트를 구독하는 컴포넌트가 마땅한 Provider
를 찾지 못 하면 기본으로 사용되는 값이다.
Context.Provider
ex)<ColorContext.Provider value="blue" > ...ManyComponent </ColorContext.Provider>
<Context.Priveder>
컴포넌트는 컨텍스트를 구독하고 있는 자식 컴포넌트에게 컨텍스트의 변화를 알리는 컴포넌트다.
value prop
을 이용해 전달한 데이터는 Provider
컴포넌트의 자식 컴포넌트 중 해당 컨텍스트를 구독하고 있는 모든 컴포넌트에게 전역적(global)인 데이터가 된다.
따라서 전달 가능한 컴포넌트의 개수에 제한 없이 전달이 가능하다.
Class.contextType
ex)class AnyClass extends React.Component { render() { return <h1>Color is {this.context}</h1> } } AnyClass.contextType = ColorContext;
클래스 컴포넌트의 contextType
프로퍼티를 이용해 컴포넌트가 컨텍스트를 구독하게 할 수 있다.
일치하는 컨텍스트 중 가장 가까이에 있는 Provider
의 value prop
을 우선으로 전달받으며, 컴포넌트 내부에서 this.context
를 이용해 데이터를 가져온다.
단점으로는 클래스 컴포넌트만 가능하며, contextType
프로퍼티를 이용하면 하나의 컨텍스트만 구독이 가능하다.
Context.Consumer
ex)class AnyClass extends React.Component { render() { return ( <ColorContext.Consumer> {color => <h1>Color is {color}</h1>} </ColorContext.Consumer> ); } }
위의 Class.contextType
과 마찬가지로 컨텍스트를 구독하는 방법 중 하나다.
차이점은 Context.Consumer
컴포넌트를 이용하면 함수 컴포넌트에서도 컨텍스트 구독이 가능하며, 하나의 컴포넌트에서 여러개의 컨텍스트 구독이 가능해진다.
그리고 Context.Consumer
컴포넌트의 자식은 무조건 함수 형태여야 한다는 것을 주의하자.
위에서 설명한 Class.contextType
과 Context.Consumer
를 이용해 컨텍스트를 구독하는 컴포넌트를 consumer
라고 부르는데, Provider
의 하위에 있는 모든 consumer
들은 부모 컴포넌트의 재렌더링 여부와 관계없이 Provider
의 value prop
에 변경이 생기면 consumer
도 다시 렌더링 된다.
이 부분은 컨텍스트를 필요로 하지 않는 컴포넌트는 업데이트가 일어나지 않게 해서 성능 최적화가 가능하다.
그리고 컨텍스트를 사용하면 컴포넌트의 재사용성이 떨어지기 때문에 항상 컨텍스트를 사용하기 전에 컴포넌트 합성으로 해결이 가능한지 먼저 생각해보자.
여기까지가 기본적인 컨텍스트 API 사용법이다. 이제 컨텍스트를 직접 사용해보자.
위에서 사용했던 코드를 컨텍스트를 활용한 코드로 다시 작성해보자.
컨텍스트를 사용하기 전과 다르게 UserWrap
컴포넌트는 UserViewer
컴포넌트로 어떠한 데이터도 전달하지 않고, Provider
와 Consumer
컴포넌트를 이용해 App
컴포넌트에서 UserViewer
컴포넌트로 직접 데이터를 전달하는 것을 볼 수 있다.
이번엔 중첩 컨텍스트와 state 끌어올리기를 사용하는 좀 더 복잡한 예제를 만들어보자.
UserContext.js
컨텍스트 객체를 외부로 내보내는 모듈이다.
index.js
entry point
가 될 파일이다.
state
를 변경하는 changeUserName()
함수를 setUserContext.Provider
컴포넌트의 value prop
으로, 그 아래에 state.name
을 value prop
으로 전달하는 UserContext.Provider
가 중첩되어서 consumer
에게 데이터를 전달하고 있다.
이제 UserWrap
컴포넌트의 모든 자식 컴포넌트는 consumer
를 통해 App
의 state
에 접근이 가능해졌다.
User.js
UserForm
컴포넌트에서 setUserContext
의 setter
는 App
의 changeUserName()
메서드, UserContext
의 name
은 App
의 state.name
이 된다.
정리하면 UserForm
의 input
엘리먼트에 변경이 생기면 이벤트 핸들러인 setter
가 호출되어 부모 컴포넌트인 App
의 state
를 변경시킨다.
그렇게 되면 UserContext.Provider
의 value prop
에 변경이 생겨 consumer
컴포넌트들이 다시 렌더링 되고, UserContext.Consumer
로 전달받은 name
값을 이용해 다시 렌더링 된다.
그럼 정상적으로 동작하는지 확인해보자.
의도했던 대로 동작하는 것을 볼 수 있다.
Context.Provider
컴포넌트의 value prop
은 이전 value
와의 차이를 비교할 때 참조(reference)를 기준으로 비교한다.
따라서 아래처럼 value prop
에 새 객체를 생성해서 전달하게 되면 이전 객체와 차이가 없다고 생각해도, 참조값이 다르기 때문에 value
가 변경된 것으로 인식되어 자식 컴포넌트에 불필요한 렌더링이 생길 수 있다.
class MyComponent extends React.Component {
render() {
return (
<Context.Provider value={{ name : 'default' }}>
...ManyComponent
</Context.Provider>
);
}
}
이런 상황을 피하기 위해 value prop
에 객체를 전달하고 싶다면 아래처럼 state
를 이용해서 객체의 참조값을 전달해주자.
class MyComponent extends React.Component {
consturctor(props) {
super(props);
this.state = {
user : { name : 'default' }
};
}
render() {
return (
<Context.Provider value={this.state.user}>
...ManyComponent
</Context.Provider>
);
}
}
참고 자료