[react] compound components pattern에 대해

Dora_ bang·2023년 12월 16일

원티드 프리온보딩을 통해 비즈니스 로직에 대해 배우던 중 compound components pattern에 대해 알게 되었다. 부모 컴포넌트에서 자식 컴포넌트의 제어할 수 있다는 것이 내가 제일 흥미롭게 생각한 점이었다. 자식 컴포넌트는 동작과 상태를 포함하고 있지만 부모컴포넌트에서 제어할 수 있어 조금 더 직관적으로 코드를 구성할 수 있겠다고 생각했다.

현재 진행 중인 프로젝트에서 부모 컴포넌트의 제어권이 낮아지고 자식 컴포넌트에서 코드를 한 눈에 알아보기 힘들어 고민하던 중 CCP에 대해 접하게 되었다.


적용하기에 앞서 compound components pattern(이하 ccp로 칭함)에 대해 알아보고자한다.

ccp는 여러 개의 컴포넌트가 함께 작동하여 하나의 부모 컴포넌트 안에서 상호작용할 수 있도록 하는 디자인 패턴이다. 추상화를 통해 기능을 분리하고 디자인 시스템을 내부적으로 적용할 수 있다.

나는 input을 ccp로 만들어보았다. 해당 프로젝트 내에서 input 타입이 다양하게 쓰이는 만큼 스타일 및 disabled, required, valid 등 에러 메세지 등 통일된 디자인 컴포넌트를 만들기 적합하다고 생각했다.


<label htmlFor='id'>아이디</label>
<input
  type={type}
  value={value}
  id={name}
  name={name}
  onChange={onChange}
  className='...'
  required
  disabled={disabled}
  {...props}
/>

disabled, focus 되었을 때 등 label과 input의 디자인이 변화가 있다. 그리고 한 페이지 내에서 최대 5 종류의 input type이 들어가기도 해서 코드가 난잡해지고 있었다,,,^_^

그리고 디자인과 개발 작업이 동시다발적으로 진행되다보니 디자인 시스템이나 색상 등 변경되는 상황이 잦았다.

(디자이너가 중간에 교체되기도 했다,, 그래서 디자인 시스템이 갈아엎어진 상황,,^_^)

위와 같은 상황이 지속되다 보니 ccp를 빠르게 적용시켜야겠다는 생각이 들었다.

Input 내부에서 상태를 공유하기 위한 context를 생성해준다.

// @/hooks/context/useInputContext

import { InputContext } from '@/components/Input';
import { useContext } from 'react';

const useInputContext = () => {
  const context = useContext(InputContext);

  if (context === undefined) {
    throw new Error('<Input /> 태그 안에서 사용해주세요.');
  }
  return context;
};

export default useInputContext;

생성한 context를 사용해 Input 컴포넌트를 생성하고, provider를 통해 상태를 공유할 수 있도록 내려준다.

// @/components/Input

export const InputContext =
  createContext<InputContextProps>(defaultInputContext);

const Input = ({ children, ...props }: InputProps) => {
  return (
    <InputContext.Provider value={{ ...props }}>
      <div className='...'}>{children}</div>
    </InputContext.Provider>
  );
};
// @/components/Input

const Text = ({ type, value, onChange, error, ...rest }: TextInputProps) => {
  const { isRequired, isDisabled } = useInputContext();
  return (
    <label htmlFor={name}>
      <input
        type={type}
        value={value}
        id={name}
        name={name}
        onChange={onChange}
        className='...'
        required={isRequired}
        disabled={isDisabled}
        {...rest}
      />
    </label>
  );
};

Input.Text = Text;

실제 적용된 코드

// @/components/LoginForm

function App() {
  const handleChange = (e) => {
    const { value, name } = e.target;
    
    setFormValue((prev) => ({...prev, [name]: value}))
  };
  
  return (
    ...
    <form onSubmit={onSubmit}>
      <Input isRequired name='userId'>
        <Input.Label>아이디</Input.Label>
        <Input.Text
          value={formValue.id}
          placeholder='아이디를 입력해 주세요'
          onChange={handleChange}
          />
      </Input>
    ...
    </form>
  );
}

export default App;



실질적으로 코드의 길이가 대폭 줄어들진 않았지만, 확실히 코드의 가독성이 이전보다 높아진 것을 느낄 수 있었다. 이번 경험을 통해 코드 가독성에 대해 다시 한 번 생각해보는 계기가 되었던 것 같다. 그리고 추상화를 통해 기능을 분리하는 게 처음엔 힘들었는데 여러 번의 실패를 통해,,ㅎ 감각적으로 더욱 더 다가간 느낌이 든다.

(처음엔 ccp에 대해 제대로 알아보지 않고 시도한 덕분에 value 값과 onChange를 제어해야하는 건가,,? 어떤 걸 제어해야하는 거지,,,하고 삽질을 오지게 했다,,ㅎㅎ)

0개의 댓글