ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ReactJS] DoIt 리액트 프로그래밍 정석 (6장)
    프론트엔드 2020. 11. 9. 17:30
    728x90

    이번 포스트에서는 6장 React의 Context에 대해서 알아보겠습니다.

     

     관찰자 패턴이란?

      Context를 이야기하기 전에 관찰자 패턴에 대해서 먼저 이야기하겠습니다.

      리액트는 기본적으로 데이터가 아래로 흐르는 단방향성의 특징이 있습니다. (Prop Drilling 패턴)

      Drilling 패턴으로 전송된 데이터는 모든 컴포넌트에서 사용되지 않을 수도 있고 누락될 수도 있습니다.

      이를 해결하기 위해 데이터를 공급자가 관리하고 해당 데이터를 원하는 컴포넌트(소비자)가 직접 공급자를

      구독하여 데이터를 얻는 관찰자 패턴을 활용합니다.

     

     Context이란?

      React에서는 Context를 활용하여 관찰자 패턴을 제공합니다.

       

     Context의 규칙

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

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

     

     

     

     Context의 공급자

      공급자 역할 컴포넌트에 공급자의 자료형(childContextTypes), 데이터 제공 함수(getChildContext)를 정의하면 됩니다.

    import Button from './Button';
    import ButtonWithContext from './ButtonWithContext';
    
    function ButtonComp() { return <Button> 버튼 </Button>; }
    function ButtonContextComp() { return <ButtonWithContext> 버튼 + 컨텍스트 </ButtonWithContext>; }
    function Comp() { return (<> <ButtonComp/> <ButtonComp/> </>); }
    
    class main 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.state.setLoading
            }; 
        }
        
        ...
        
        render() {
        	return (
                <div>
                	<Comp />
                    <ButtonComp onPress={this.toggleLoading}>상태 변경</Button>
                </div>
            )
        }
        
        main.childrenContextTypes = {
        	loading: PropTypes.bool,
            loading: PropTypes.func,
        }
    }

     

      위의 소스를 그대로 사용해도 되나 좀 더 코드를 간결하게 작성해보겠습니다.

      Provider 컴포넌트에 공급자에 필요한 소스들을 구성해두고 원래의 main 컴포넌트를 래핑 하도록 하겠습니다.

    class Provider extends React.Component {
        constructor(props) { ... }
        
        getChildContext() { ... }
        
        ...
        
        LoadingProvider.childContextTypes = { ... }
    }
    
    export default Provider;
    import Provider from './Provider';
    
    class main extends PureComponent {
        ...
        
        render() {
        	return (
                <Provider>
                	<Comp />
                    <ButtonComp onPress={this.toggleLoading}>상태 변경</Button>
                </Provider>
            )
        }
    }

     

     Context의 소비자

      소비자 역할 컴포넌트에 contextTypes를 명시하여 데이터 구독하면 됩니다.

      모든 소비자 컴포넌트에 contextTypes를 명시하는 것은 비효율적이니 하이어 오더 컴포넌트로 구성하겠습니다.

    export default WrappedComponent => {
        ...
        
        function WithContext(props, context) {
        	const { loading, setLoading } = context;
            return {
            	<WrappedComponent {...props} loading={loading} setLoading={setLoading} />
            };
        };
        
        WithContext.contextTypes = {
        	loading: PropTypes.bool,
            setLoading: PropTypes.func,
        };
        
        return WithContext;
    }
    import withContext from './withContext';
    
    function ButtonContextComp({ label, loading, setLoading }) {
        return (
            <Button onPress={() => setLoading(!loading)}>
                {loading ? '로딩 중' : label}
            </Button>
        );
    }
    
    ...
    
    export default withContext(ButtonContextComp);

     

    ✔ 중복 공급자

     만일 소비자가 여러 개의 공급자 데이터를 구독한다면 어떻게 될까요?? 정답은 가까운 공급자의 데이터를 접근합니다.

     이 문제를 해결하기 위해선 키 값을 도입해서 소비자가 원하는 공급자의 데이터를 반환하는 커링 함수를 생성하면 됩니다.

     소스를 보시면 기존의 Provider에 키 값 인자를 받아 키 값에 맞추서 공급자의 자료형을 생성하면 됩니다.

    const DEFAULT_KEY = 'DEFAULT_KEY';
    
    export default (contextKey = DEFAULT_KEY) => {
    	class Provider extends React.Component {
            constructor(props) { ... }
    
            getChildContext() {
                return { 
                    [contextKey]: {
                        loading: this.state.loading,
                        setLoading: this.setLoading
                    }
                }
            }
            
            ...
            
            LoadingProvider.childContextTypes = { [contextKey] : contextPropTypes }
        }
        
        return Provider;
    } 
    import Provider from './Provider';
    
    const Provider1 = Provider('1');
    const Provider2 = Provider('2');
    
    class main extends PureComponent {
        ...
        
        render() {
        	return (
                <Provider1>
                    <Provider2>
                        <Comp />
                        <ButtonComp onPress={this.toggleLoading}>상태 변경</Button>
                    </Provider2>
                </Provider1>
            )
        }
    }

     

     소비자도 마찬가지로 변경된 공급자 구조에 맞게 키 값에 맞추어 소스를 작성해주시면 됩니다.

    export default (contextKey = DEFAULT_KEY) => WrappedComponent => {
        ...
        
        function WithContext(props, context) {
        	const { loading, setLoading } = context[contextKey];
            return {
            	<WrappedComponent {...props} loading={loading} setLoading={setLoading} />
            };
        };
        
        WithContext.contextTypes = {
        	[contextKey] : contextPropTypes
        };
        
        return WithContext;
    }

     

      그런데 사실 이렇게 공급자와 소비자를 직접 구현할 필요는 없습니다.

      React에서는 Context-API를 통해 위의 내용을 좀 더 쉽게 활용 가능하기 때문입니다.

      하지만 한 번쯤 직접 구현해보시면 Context-API 동작원리 이해하는데 큰 도움이 될 겁니다.

     

    ✔ Context-API

      앞서 직접 구현했던 공급자와 소비자는 Context-API의 createContext() 함수를 통해 손 쉽게 사용할 수 있습니다.

      앞서 하나씩 정의했던 getChildContext(), childrenContextTypes 모두 API에서 자동으로 관리해줍니다.

      소스를 보시면 context를 정의해서 공급자가 공유할 데이터를 정의합니다.

      앞서의 getChildContext + childrenContextTypes 를 합친 것과 비슷한 역할을 합니다.

      그리고 마지막으로 value를 통해 자식 컴포넌트에 컨텍스트 데이터를 전달합니다.

      소비자는 아래처럼 컨택스트의 데이터에 접근해서 사용하면 됩니다.

    const { Provider, Consumer } = React.createContext(defaultValue);
    
    const { Provider, Consumer } = React.createContext({});
    
    class Provider extends React.PureComponent {
        constructor(props) { ... }
        
        ...
        
        render() {
        	const context = {
                ...this.state,
                setLoading: this.setLoading
            };
            
            return (
                <Provider value={context}>
                    {this.props.children}
                </Providre>
            )
        }   
    }
    
    export default Provider;
    import { Consumer } from './Provider';
    
    function ButtonConsumer({ children }) {
        ...
        
        return (
        	<Consumer>
              {(contextValue) => (
                  <Button> {contextValue.loading ? 'now loading...' : children} </Button>
              )}
            </Consumer>
        )
    }

     

Designed by Tistory.