본문 바로가기

개발 이야기/front-end

Component Pattern | Context API 를 활용하자.

Prop Drilling

참고: https://kentcdodds.com/blog/prop-drilling

 

하나의 컴포넌트를 여러 컴포넌트로 쪼개고, 쪼갠 컴포넌트에서 또 쪼개는 것. 하나의 컴포넌트에서 전부 다뤄지던 state가 props로 전달 된다. 깊이가 깊어질 수록 prop을 전달하는 과정이 드릴로 뚫고 들어가는 것 같아서 Prop Drilling이라고 한다.

 

Card 
→ CardContent, CardBottom

CardContent 
→ Title, Description, Thumbnail

CardBottom 
→ LikeButton, HateButton

 

 

좋은 점

한 컴포넌트 안에서 코드가 매우 길어지면 수정이 어려워진다. global로 변수를 관리할 때처럼 하나의 컴포넌트 안에 여러 state와 function이 있어 각각 어떤 역할을 하는지 경계가 모호해진다. 그러므로 기능 별로 컴포넌트를 쪼개어 더 작은 영역 안에서 컴포넌트를 관리한다.

 

나쁜 점

  • 서비스가 커질 수록 컴포넌트의 depth가 깊어질 수 있다.
  • props를 여러번 전달하면서 빠지는 props가 생길 수도 있다.

 

해결 방법

전체 서비스도 사실 하나의 render function 안에 정의할 수 있다. 권장되지는 않지만 그 의미를 생각해봐야 한다. 하나의 컴포넌트 안에서도 코드가 잘 정리되어 있거나 혹은 잘 수정되지 않는 코드의 경우 굳이 drilling할 필요가 없다. 즉, 성급하게 쪼개지 말고, 재사용이 필요해질 때 쪼개야 한다.

또한, 필요한 경우 drilling을 했을 때 컴포넌트 depth가 너무 깊어진다면 React Context API를 사용해서 props를 계속 전달하지 않고, 필요한 컴포넌트에서만 state를 사용한다.

 

활용 사례

Card 컴포넌트를 리팩토링하려고 컴포넌트 안에서 비중이 큰 Thumbnail, CardBottom 부분만 따로 분리했다. CardBottom 부분도 꽤 커서 그 안에서도 다시 BookmarkButton, MoreMenuButton으로 분리했다. 여기에서 context API를 써서 props depth가 깊어지는 것을 피하고 필요한 곳에만 사용할 수 있다.

 

 


 

 

Component Patterns

참고: https://dev.to/alexi_be3/react-component-patterns-49ho

✨내가 개발하는 컴포넌트가 재사용될 것을 항상 염두해 두고 컴포넌트를 개발해야 한다.

 

Compound Components

부모와 자식 컴포넌트가 내부적으로 state를 공유한다. select, option 태그가 하나의 그룹으로서 자주 사용되듯이, compound component도 직렬(tandem)로 연결되어 하나의 컴포넌트처럼 동작한다.

 

 

{/* 
code from alexi_be3 on dev.to

The parent component that handles the onChange events 
and managing the state of the currently selected value. 
*/}
<RadioImageForm>
  {/* The child, sub-components. 
  Each sub-component is an radio input displayed as an image
  where the user is able to click an image to select a value. */}
  <RadioImageForm.RadioInput />
  <RadioImageForm.RadioInput />
  <RadioImageForm.RadioInput />
</RadioImageForm>

 

 

 

예시: 폼 안의 자식 컴포넌트 디테일은 abstract하게 구현하고, consumer 쪽에서 디테일하게 구현하도록 할 수 있다. (code from alexi_be3)

*hook으로 구현하는 법 (codepen from alexi_be3)

 

 

부모 코드

export class RadioImageForm extends React.Component<Props, State> {

  static RadioInput = ({
    currentValue,
    onChange,
    label,
    value,
    name,
    imgSrc,
    key,
  }: RadioInputProps): React.ReactElement => (
    //...
  );

  onChange = (): void => {
    // ...
  };

  state = {
    currentValue: '',
    onChange: this.onChange,
    defaultValue: this.props.defaultValue || '',
  };

  render(): React.ReactElement {
    return (
      <RadioImageFormWrapper>
        <form>
        {/* .... */}
        </form>
      </RadioImageFormWrapper>
    )
  }
}

 

 

 

자식 컴포넌트 처리 (cloneElement 설명)

render(): React.ReactElement {
  const { currentValue, onChange, defaultValue } = this.state;

  return (
    <RadioImageFormWrapper>
      <form>
        {
          React.Children.map(this.props.children, 
            (child: React.ReactElement) =>
              React.cloneElement(child, {
                currentValue,
                onChange,
                defaultValue,
              }),
          )
        }
      </form>
    </RadioImageFormWrapper>
  )
}

 

 

사용할 때 : RadioImageForm state를 공유 받을 수 있음

<RadioImageForm onStateChange={onChange}>
  {DATA.map(
    ({ label, value, imgSrc }): React.ReactElement => (
      <RadioImageForm.RadioInput
        label={label}
        value={value}
        name={label}
        imgSrc={imgSrc}
        key={imgSrc}
      />
    ),
  )}
</RadioImageForm>

 

 

단, RadioInput이 아주 깊은 div 무덤에 갇히거나, consumer component에서 구조를 바꾸려한다면 어떨까? 컴포넌트는 여전히 렌더링 되지만 RadioImageForm의 state를 정상적으로 공유받을 수 없다.

 

 

Flexible Compound Components

기존 compound component의 문제점은 포맷이 깨지면 state를 받을 수 없다는 것이다. flexible하지 않다. 그러므로 컴포넌트 트리 구조에 상관 없이 state를 공유받을 수 있도록 React Context API 사용

 

 

RadioImageForm.tsx 파일에 context 생성

const RadioImageFormContext = React.createContext({
  currentValue: '',
  defaultValue: undefined,
  onChange: () => { },
});
RadioImageFormContext.displayName = 'RadioImageForm';

 

 

 

render 구현

render(): React.ReactElement {
  const { children } = this.props;

  return (
    <RadioImageFormWrapper>
      <RadioImageFormContext.Provider value={this.state}>
        {children}
      </RadioImageFormContext.Provider>
    </RadioImageFormWrapper>
  );
}

 

 

 

state가 바뀔 때마다 컴포넌트를 리렌더링한다. 아래처럼 state에 object를 넘기면 object가 바뀌지 않아도 매번 리렌더링 된다. (object는 렌더링마다 새로운 오브젝트를 생성하고 재할당되므로 다른 object로 인식된다)

⚠️DON'T TRY THIS AT HOME

<RadioImageFormContext.Provider value={{ currentValue: this.state.currentValue, onChange: this.onChange }}>

 

 

Provider Pattern

uni-directional data flow (양방향 데이터 플로우)로 동작하는 경우, state를 공유하는 prop drilling을 할 수 밖에 없다. (부모 → 자식) 이런 경우 스파게티 코드가 될 수 밖에 없다.

 

상태 관리 라이브러리들이 있지만, 이런 내용을 이해하지 못한 채 상태 관리 라이브러리를 사용하여 코드 베이스를 더 복잡하게 만들 필요는 없다.