TIL 22-04-08 (2)

thisisyjin·2022년 4월 8일
0

TIL 📝

목록 보기
16/113

React.js

Today I Learned ... react.js

🙋‍♂️ React.js Lecture

🙋‍ My Dev Blog


Context

컨텍스트 기초 개념

  • propsstate = 부모-자식 컴포넌트가 연결된 상태에서 공유하는 데이터
  • cf> 부모-자식 컴포넌트가 연결되어 있지 않을때 context를 통해 데이터 공유 가능
  • 컨텍스트는 데이터 공유 저장소데이터 전파를 담당.

컴포넌트의 구조

  • 컴포넌트는 트리 구조 형태를 띄고 있음.

  • 상위 -> 하위 컴포넌트로 전달시 props 사용.

  • 최상위 컴포넌트에서 최하위 컴포넌트로 props를 전달하려면 그 중간 컴포넌트들을 모두 거쳐야 한다.
    -> 만약 중간 컴포넌트에서 그 props를 사용하지 않거나 전달과정에서 누락이 될 시, 오류 발생.

  • props를 원하는 컴포넌트에 바로 전달하려면?
    🔻

관찰자 패턴 (Observable Pattern)

  • 데이터는 공급자가 관리하고, 관찰자는 공급자를 구독하여 데이터를 얻는 방식.
  • 관찰자 패턴을 도입시, 공급자와 관찰자가 서로 역할을 분리하여 데이터를 관리.
공급자관찰자 (=소비자)
데이터 보관, 변경, 소비자에게 데이터 공급공급자를 구독하여 데이터를 소비
  • 최상위 컴포넌트에서 출발한 props인 'data'는 공급자에 보관된다.
  • 공급자는 'data'를 소비자(=최하위 컴포넌트, 즉 전달할 컴포넌트) 에게 전달한다.
    -> 컴포넌트 간의 자료 의존성이 없어짐.
  • 공급자는 최상위 컴포넌트 하위에 포함되어있고, 소비자는 하이어오더 컴포넌트이며 RowCComponent 하위에 있는 구조.

공급자와 소비자

1. 소비자는 공급자보다 낮은 계층에 있어야 한다.
-> 소비자가 공급자보다 상위에 있으면, 공급자를 구독할 수 없다.

2. 소비자는 공급자가 제공하는 콜백 함수로 데이터를 변경할 수 있다.
-> 소비자는 공급자의 하위계층이지만, 공급자의 데이터를 변경할 수 있다.
-> 직접 변경은 X. 공급자로부터 데이터 변경을 위한 콜백을 받아 데이터 변경 요청을 함.

  • 공급자는 소비자에게 콜백함수를 전달함.
  • 소비자는 콜백함수에 인자로 자신이 속한 컴포넌트의 데이터를 전달함.(변경 요청)

-> 공급자의 데이터가 변경되었으므로 소비자와 동일하게 맞춤.
-> Data Down, Action Up (DDAU) 라고 부름.

  • 단방향 데이터 흐름 - 데이터의 일관성을 유지


1. 공급자 구현

  • 데이터 제공함수 getChildContext 정의
  • setState를 포함하는 함수를 소비자에게 전달하여 변경 가능하게 함. (setLoading)

/src/component/ctx/HomePageComponent.jsx

// 공급자 구현 - 공급자 역할을 할 공급자의 자료형(childContextTypes)과, 데이터 제공함수(getChildContext) 정의

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

function RowBComponent() {
  return <Button>버튼</Button>;
}

function RowCComponent() {
  return <ButtonWithContext>버튼</ButtonWithContext>;  // 소비자인 ButtonWithContext를 출력
}

function TableComponent() {
  return (
    <table>
      <RowBComponent />
      <RowCComponent />
    </table>
  );
}

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

    this.state = { loading: false };
    this.setLoading = this.setLoading.bind(this);  // setLoading을 소비자에 전달하여 데이터를 변경할 예정.
    // this를 바인딩항 소비자에서 공급자의 setState()에 접근할 수 있도록 함.
    this.toggleLoading = this.toggleLoading.bind(this);
  }

  getChildContext() {  // 소비자는 getChildContext로 loading과 setLoading을 전달받을 것임.
    return {
      loading: this.state.loading,
      setLoading: this.setLoading,
    };
  }

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

  toggleLoading() {   
    this.setState(({ loading }) => ({ loading: !loading }));
  }

  render() {
    return (
      <div>
        <TableComponent />
        <Button onPress={this.toggleLoading}>상태 변경</Button> { // toggleLoading을 실행하는 버튼 }
      </div>
    );
  }
}

HomePageComponent.childContextTypes = {  // 컨텍스트의 자료형 정의
  loading: PropTypes.bool,
  setLoading: PropTypes.func,
};

export default HomePageComponent;

참고 - onClickonPress

onPress = { () => this._onPress(key) }
  • onPress는 HTML 어트리뷰트 onClick과 같지만, 콜백함수를 값으로 받는다.
  • 즉, 함수의 리턴값이 아닌 함수 자체를 값으로 전달해줘야 한다.

참고 2 - PropTypes

  • propTypes - props의 타입을 정의
  • defaultProps - props의 기본값 정의
  • childContextTypes - context의 타입 정의

2. 소비자 구현

  • 소비자는 하이어오더 컴포넌트로 구현함.
  • 소비자의 역할을 할 컴포넌트에 contextTypes 속성을 추가하여 공급자에서 구독할 항목 정의.
    -> 매번 contextTypes 속성을 추가하면 불편하므로, 하이어오더 컴포넌트를 도입.

2-1) 소비자를 반환하는 하이어오더 컴포넌트 구현

  • 지난번에 작성했던 withLoading을 수정하여, 소비자를 반환하는 하이어오더 컴포넌트를 구현.
  • context를 전달받아 구조분해할당 -> WrappedComponent의 props로 전달

/src/component/ctx/WithLoadingContext.jsx

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

export default (WrappedComponent) => {   // 하이어오더 컴포넌트임. (WithLoadingContext를 리턴함)
  const { displayName, name: componentName } = WrappedComponent;
  const wrappedComponentName = displayName || componentName;

  function WithLoadingContext(props, context) {
    const { loading, setLoading } = context;
    return (
      <WrappedComponent {...props} loading={loading} setLoading={setLoading} /> { // ✅ context로 받은 객체를 직접 프로퍼티로 전달함.}
    );
  }
  WithLoadingContext.displayName = `withLoadingContext(${wrappedComponentName})`;
  WithLoadingContext.contextTypes = {
    loading: PropTypes.bool,
    setLoading: PropTypes.func,
  };
  return WithLoadingContext;
};

2-2) 하이어오더 컴포넌트로 소비자 만들기

  • Button 컴포넌트를 소비자로 만들기.
  • 위에서 만든 WithLoadingContext(하이어오더 컴포넌트)에 Button 컴포넌트를 인자로 전달하여 소비자 구현.

/src/component/ctx/ButtonWithLoadingContext.jsx

import React from 'react';
import PropTypes from 'prop-types';
import Button from '../Button';
import WithLoadingContext from './WithLoadingContext';

function ButtonWithLoadingContext({ label, loading, setLoading }) {  // 공급자의 데이터를 props로 전달받음
  return (
    <Button onPress={() => setLoading(!loading)}>
      {loading ? '로딩 중' : label}
    </Button>
  );
}

ButtonWithLoadingContext.propTypes = {
  label: PropTypes.string,
  loading: PropTypes.bool,
  setLoading: PropTypes.func,
};

export default WithLoadingContext(ButtonWithLoadingContext);


3. 컴포넌트에서 공급자 분리

  • 공급자 구현시 공급자를 HomePageComponent(최상위 컴포넌트)에 초함시켰음.
  • 공급자를 분리하여 PureComponent가 아닌 Component로 사용.
    -> PureComponent는 context 변경에 대한 비교가 생략되어 있음.
    -> 하위 공급자에 포함되어있는 컴포넌트들이 동기화되지 ❌ (render 호출 안함)

/src/component/ctx/LoadingProvider.jsx

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

class LoadingProvider extends React.Component {
  constructor(props) {
    super(props);

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

  getChildContext() {  // HomePageComponent에 사용된 공급자 데이터 항목을 옮겨놓음
    return {
      loading: this.state.loading,
      setLoading: this.state.setLoading,
    };
  }

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

  render() {
    return this.props.children;  /// 자식 props 노드를 출력함.
  }
}

LoadingProvider.childContextTypes = {
  loading: PropTypes.bool,
  setLoading: PropTypes.func,
};
export default LoadingProvider;

4. HomePageComponent에서 공급자 코드 삭제

  • 공급자를 따로 분리했으니 (LoadingProvider.jsx), 기존에 HomePageComponent에 있던 공급자 코드를 삭제.

/src/component/ctx/HomePageWithProvider

import React, { PureComponent } from 'react';
import Button from '../Button';
import ButtonWithLoadingContext from './ButtonWithLoadingContext';  // 소비자 컴포넌트
import LoadingProvider from './LoadingProvider';  // 공급자 컴포넌트

function RowBComponent() {
  return <Button>버튼</Button>;
}

function RowCComponent() {
  return <ButtonWithLoadingContext>버튼</ButtonWithLoadingContext>;  // 소비자 출력
}

function TableComponent() {
  return (
    <table>
      <RowBComponent />
      <RowCComponent />
    </table>
  );
}

class HomePageComponent extends PureComponent {
  render() {
    return (
      <LoadingProvider>  // 공급자
        <TableComponent />
        <ButtonWithLoadingContext>상태 변경</ButtonWithLoadingContext>  // 소비자 출력
      </LoadingProvider>
    );
  }
}

export default HomePageComponent;

5. 스토리북에 추가

/src/stories/ContextStory.jsx

import React from 'react';
import { storiesOf } from '@storybook/react';

import HomePageComponent from '../component/ctx/HomePageComponent';
import HomePageWithProvider from '../component/ctx/HomePageWithProvider';

storiesOf('HomePageComponent', module)
  .add('컨텍스트', () => <HomePageComponent />)
  .add('Provider', () => <HomePageWithProvider />);
profile
기억은 한계가 있지만, 기록은 한계가 없다.

0개의 댓글