Context를 이용한 공급자, 소비자 관찰자 패턴

zzwwoonn·2022년 6월 23일
0

React

목록 보기
20/23

프로퍼티와 스테이트는 부모 컴포넌트와 자식 컴포넌트가 연결되어 있는 상태에서 (=> 종속적인 관계가 확립됐을 때) 공유하는 데이터였다.

반면 컨텍스트는 부모와 자식 컴포넌트가 연결되어 있지 않아도 데이터를 공유할 수 있게 해준다. 보통 컨텍스트는 '데이터 공유 저장소'와 '데이터 전파'의 역할을 수행한다.

이전에 Redux에 대해서 공부하고 포스팅을 했었다. 똑같은? 비슷한 맥락의 개념이지만 그 때는 React Redux - useReducer Hook에 대해 자세히 알아봤었고 이번에는 컨텍스트를 직접적으로 이용, 접목시킨 소비자, 공급자 (관찰자 패턴)에 대해 자세히 알아보려고 한다.

하지만?

리액트 공식문서에서는 컨텍스트를 직접적으로 다루는 것을 권장하지 않는다. 따라서!? 컨텍스트의 개념, 어떻게 쓰는지, 데이터는 어떤식으로 전달되는지 정도만 잘 이해하고 이런게 있구나~ 하고 넘어가자.

리액트 공식문서 - Context

컨텍스트가 필요한 이유

컴포넌트는 트리의 구조를 띠고 있다. 예를 들어 홈페이지 밑에 로그인 & 회원가입 페이지 밑에 로그인 창 으로 계속해서 타고 내려가는 구조를 띈다.

상위(부모) 컴포넌트의 데이터가 하위 컴포넌트로 전달 될 때 프로퍼티(props)를 사용하며 데이터 전달에 문제가 없어 보인다.

페이지1 > 페이지2 > 페이지3 > 페이지4 와 같은 컴포넌트 구조(트리, 최상위 부모 컴포넌트 = 페이지1)가 있을 때

페이지1에서 페이지4로 데이터를 전달하고 싶다면 페이지1에서 페이지2로 프로퍼티를 넘겨주고 페이지2에서 페이지3으로 프로퍼티를 넘겨주고... 이렇게 타고타고 내려가야 한다.

이 때 전달하는 데이터가 페이지2 에서는 쓰이지도 않지만 중간 다리의 역할을 수행해야 하기 때문에 어쩔 수 없이 프로퍼티를 넘겨줘야 하는 문제점이 있을 수 있고, 중간 다리의 한 부분에서 프로퍼티로 데이터를 넘겨주는 과정이 누락된다면 리액트 앱은 곧바로 에러를 뱉어낸다.

쓰고자 하는 데이터를 원하는 컴포넌트에 직접적으로 전달할 방법이 있나? 해서 나온게 바로 컨텍스트(Context) 개념이다.

공급자-소비자 간의 관계

관찰자 패턴 (Observable Pattern)
"데이터는 공급자가 관리하고 관찰자는 공급자를 구독하여 데이터를 얻는 방식"을 관찰자 패턴이라 한다. 관찰자 패턴을 도입하면 공급자와 관찰자가 서로 역할을 분리하여 데이터를 관리하게 된다.
공급자는 데이터 보관, 데이터 변경, 소비자에게 데이터를 공급하는 역할을 하고, 관찰자는 공급자를 구독하여 데이터를 소비하는 역할을 담당한다.

소비자는 공급자보다 낮은 계층에 있어야 한다

소비자는 상위에 정의되어 있는 공급자만 구독할 수 있다. 소비자가 공급자보다 상위에 있으면 공급자를 구독할 수 없다.
쉽게 말하면 공급자의 자식으로 소비자가 있어야 하는 것이다.

소비자는 공급자가 제공하는 콜백 함수로 데이터를 변경할 수 있다.

소비자는 공급자의 데이터를 변경할 수 있는데 단, 소비자가 공급자의 데이터를 변경할 때는 '공급자의 데이터에 직접 접근하여 변경하는 것'이 아니라 '공급자로부터 데이터 변경을 위한 콜백 함수를 받아 데이터 변경 요청'을 해야 한다.

소비자는 공급자가 전달해준 콜백 함수에 해당 컴포넌트의 바꾸고자 하는 데이터를 인자로 전달하여 공급자의 데이터를 변경한다. 즉, 소비자는 공급자가 전달한 콜백 함수로 데이터 변경 요청을 하는 것이다.

공급자의 데이터가 변경되었으므로 소비자와 맥락? 데이터 현황을 동일시 해야 한다. 이 과정을 Data Down, Action Up(데이터는 아래로, 변경 요청은 위로) 이라 하고 줄여서 DDAU 라고 부른다.

이와 같은 단방향 데이터 흐름은 변경된 데이터의 일관성을 유지하는데 효과적이다.

구현하고자 하는 컴포넌트의 구조

공급자 구현

공급자를 구현하는 방법은 공급자 역할을 할 컴포넌트에 공급자의 자료형 (=> childContext Types)와 데이터 제공 함수 (=> getChildContext())를 정의하며 된다.

함수형 컴포넌트로는 공급자를 구현할 수 없기 때문에 클래스형 컴포넌트로 공급자를 구현한다.

컨텍스트를 이용했을 때 ⇒ RowCComponent 와 컨텍스트를 이용하지 않고 props 로 데이터와 set 함수를 넘겨주면서 동작할 때를 비교한다.


// HomePageComponent.jsx

import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import ButtonWithContext from './ButtonWithContext';

function RowBComponent(props) {
	return (
		<button
			onClick={() => {
				console.log('123');
				props.toggleLoading();
			}}
		>
			상태 변경 - 컨텍스트 안쓰고
		</button>
	);
}

function RowCComponent() {
	return <ButtonWithContext>버튼 - 컨텍스트 쓰고</ButtonWithContext>;
}

function TableComponent(props) {
	return (
		<>
			<RowBComponent loading={props.loading} toggleLoading={props.toggleLoading} />
			<RowCComponent />
		</>
	);
}

class HomePageComponent extends PureComponent {
	constructor(props) {
		super(props);

		this.state = { loading: false };
		this.setLoading = this.setLoading.bind(this);
		this.toggleLoading = this.toggleLoading.bind(this);
	}

	getChildContext() {
		return {
			loading: this.state.loading,
			setLoading: this.setLoading,
		};
	}

	setLoading(loading) {
		this.setState({ loading });
	}

	toggleLoading() {
		console.log('123');
		this.setState(({ loading }) => ({ loading: !loading }));
	}

	render() {
		return (
			<div>
				<TableComponent loading={this.state.loading} toggleLoading={this.toggleLoading} />
				<p>{this.state.loading ? 'O' : 'X'}</p>
			</div>
		);
	}
}

HomePageComponent.childContextTypes = {
	loading: PropTypes.bool,
	setLoading: PropTypes.func,
};

export default HomePageComponent;

// ButtonWithContext.jsx 

import React from 'react';
import PropTypes from 'prop-types';

function ButtonWithContext({ children }, context) {
	const { loading, setLoading } = context;
	console.log('context = ', context);
	return (
		<>
			
			<button onClick={() => setLoading(!loading)}>
				{loading ? '로딩 중 ? 현재 - O' : children}
			</button>
		</>
	);
	// return <button onClick={() => setLoading(!loading)}>{loading ? 'O' : 'X'}</button>;
}

ButtonWithContext.contextTypes = {
	loading: PropTypes.bool,
	setLoading: PropTypes.func,
};

export default ButtonWithContext;

3 => 콜백 함수 setLoading()을 소비자에 전달하여 데이터를 변경할 예정이므로 공급자의 this를 바인딩 해준다. 만약 this를 바인딩하지 않으면 콜백 함수가 실행되는 소비자 컴포넌트(ButtonWithContext)에서 공급자의 setState() 함수에 접근하지 못하므로 오류가 생긴다.

콜백 함수의 this 범위로 인해 생기는 오류 (클래스 내부에서의 this는 일반적인 상황에서 전역 객체를 가리켜 버리기 때문에 명시적으로 bind를 통해 this를 가리켜줘야 한다.) 피하기 위해 bind 함수를 사용하여 this 객체를 전달하는 과정을 포함한다.
- ES6 Grammar Essentials (b)

4 => 소비자는 getChildContext() 함수를 통해 loading 과 setLoading() 함수를 전달 받는다.

두 가지 방법 모두 잘 올바르게 동작한다.

0개의 댓글