디자인패턴

React 컴포지션 패턴

데일리코딩 2024. 5. 25. 17:12

개요

대다수의 개발자들은 si로 개발하는것 보다 실제 기능을 추가하는 일이 더 많을 것이다

하지만 막상 기능을 추가하거나 수정하는일에 선뜻 나서기가 어렵다

그 이유는 

 

수정 후 (혹은 기능 추가 후) 다른곳에서 어떻게 동작할지 예측이 어렵다

 

즉 Side Effect의 위험이 도사리고 있기 때문이다

그런 위험을 최대한 낮추고 빠르게 기능을 수정 및 추가하기위해 각 디자인 패턴들이 존재하고 

이번 포스트에는 컴포지션이라는 패턴을 학습하고 실제 업무에 적용하고 느낀점을 공유하고자 

포스팅을 올리게 되었다.

 

실제 코드와 함께 적용한 내용을 정리하면서 최대한 보기 쉽게 공유하고자 한다.

 

React 컴포지션 패턴이란

React에서 컴포넌트는 하나의 모듈을 의미하는데 모듈을 마치 레고의 하나의 온전한 부품이라고 비유할 수 있다.

즉 컴포지션 패턴은 여러개의 부품을 합쳐 하나의 큰 모듈로 구성할 수 있게 해준다.

 

이렇게 작은 모듈로 나눠서 하나의 큰 모듈로 만들어야하는 이유가 뭘까

수많은 이유가 있지만 모든 이유를 관통하는 핵심 이유는 바로 적은 유지보수의 비용 이라고 생각한다

다음 내용을 통해 컴포지션을 통해 얻을 수 있는 장점들을 알아보자

컴포지션 패턴의 장점

1. 해당 모듈들을 재사용 할 수 있다. 

2. 수정이 필요한 경우 해당 모듈만 수정하면 된다.

3. 수정에는 닫히고 확장성이 높아진다.

4. 각 컴포넌트들의 1개의 책임만 들고 갈 수 있다.  따라서 해당 모듈이 하는 역활이 명확해지고 이해하기가 쉬워진다.

 

실제 코드와 함께 보는 컴포지션 패턴 구현 방법

TableInput 컴포넌트를 통해 상세 내용을 추상화 시켰고 안에 내용을 살펴보면 다음 코드를 확인 할 수 있다.

//TableInput 컴포넌트

<CTableDataCell className="sf-label" color="gray">
	회원 이메일
</CTableDataCell>
<CTableDataCell className="sf">
    <CFormInput
    {...register("inst_mem_email")}
    defaultValue={data?.inst_mem_email}
    type="email"
    size="sm"
    className="lh-15"
    disabled={ReadOnly}
    readOnly={ReadOnly}
    />
	<span>{errors?.error.inst_mem_email}</span>
</CTableDataCell>

 

해당 컴포넌트는 목적과 역확을 나눠서 본다면 현재 3가지 일을 하고 있다

1. 회원 이메일 label을 보여주고 있고

2. 이메일 Input을 역활

3. 마지막 해당 에러를 보여주는 역활

총 3가지의 역활을 하나의 컴포넌트에서 책임지고 있다.

 

만약 이런 TableInput 컴포넌트가 모든 사이트 전체에 이용되고 있다고 가정 하에 

에러를 표현하는 특정 페이지에서는 내용이 변경되거나, 혹은 label 쪽의 디자인을 변경, 수정한다고 한다면

Input 쪽은 변경되는것이 없는데 label, 에러 메세지 새로 수정해서 새로운 컴포넌트를 만들어서 

해당 페이지에 적용하는것은 뭔가 아쉽다 input쪽을 마치 레고의 하나의 부품처럼 쏙 빼서 새로운 label, 에러 메세지 조립 하면

좋지 않을까?

 

그럼 props 를 통해 에러화면을 보여주는 부분과 label을 부분을 컴포넌트로 받아와서 처리한다면 ?

해당 컴포넌트는 에러화면을 보여주는 컴포넌트와 label 컴포넌트에 의존적으로 변경이 될 수 밖에 없고 

TableInput의 재 사용성은 떨어질 것이다 (다양한 상황에서 대처할 수 없다)

 

이때 컴포지션을 사용해서 다양한 상황과 수정에 대응 할 수 있는 코드로 변경할 수있다. 

 

const TextInput = ({ name, value, isReadOnly, register, registerOptions }) => {
    return (
      <>
        <CTableDataCell className="sf">
          <CFormInput
            {...register(name, registerOptions.name)}
            defaultValue={value}
            size="sm"
            className="lh-15"
            disabled={isReadOnly}
            readOnly={isReadOnly}
          />
        </CTableDataCell>
      </>
    )
  }

  const withError = (Component) => {
    return ({ error, name, registerOptions, ...rest }) => (
      <>
        <Component name={name} registerOptions={registerOptions} {...rest} />
        {error?.name && <span>{error.name.message}</span>}
      </>
    )
  }

  const withCol = (Component) => {
    return ({ colName, name, ...rest }) => (
      <>
        <CTableDataCell className="sf-label" color="gray">
          {colName}
        </CTableDataCell>
        <Component name={name} {...rest} />
      </>
    )
  }
  
  const TextInputWithErrorWithCol = withError(withCol(TextInput))
  const TextInputWithError = withError(TextInput)
  const TextInputWithCol = withCol(TextInput)
  
  만약 withCol 부분을 새롭게 개발한다?
  
  const withNewCol = (Component) => {
    return ({ colName, name, ...rest }) => (
      <>
        <CNewDataCell className="sf-label" color="gray">
          {colName} 새로운 내용 추가!
        </CNewDataCell>
        <Component name={name} {...rest} />
      </>
    )
  }
  
 const TextInputWithNewCol= withNewCol(TextInput)

 

 

자 완성된 코드는 위와 같다 

우선 SOLID의 단일책임 원칙의 개념으로 각 모듈을 바라보면

각각의 모듈이 책임지는 목적을 1개로 설정했다

 

자 해당 코드를 통해 얻는 이점을 정리하겠다.

TextInput은 더이상 3개의 역활을 할 필요가 없어 코드가 간결해졌다

withCol 모듈은 테이블의 컬럼 label을 표현하는 1가지 책임

withError 모듈은 error 객체를 가져와서 에러를 표현하는 책임 1가지

TextInput은 input의 역할 하나만 하면 된다

이로써 각 부품의 수정또한 용이해지고 보기 간결해진다. 

 

TextInput 은 더이상 에러메세지 와 label 컴포넌트에 의존성을 제거하여 다른곳에서도 재사용 할 수 있다.


이때 만약 어느 특정페이지에서는 다른 label이 필요하다면 다른 부품들은 수정할 필요 없이 그저 새로운 withNewCol 부품만 생성해서

마치 조립만 하면 된다

이때 가장 중요한 핵심은 조립이다

 

withCol 부품과 withError 부품이 서로 의존하지 않아 TextInput 도 있어도 그만 없어도 그만인 상황이다

언제든지 쉽게 withCol 부품만 조립해도 되구 혹은 withError 부품만 조립해도 작동한다.

 

이로써 각 부품들의 재사용성도 높아졌다

새로운 부품이 필요하다면 새롭게 개발하고 다시 기존의 다른 부품들과 조립하면 된다.

 

이것이 컴포지션 패턴의 가장 핵심이 아닌가 싶다.

 

수정에는 닫혀있고 확장성이 높아졌다.

새로운 요구사항에 lable 컴포넌트에 변경이 되어도 TextInput 컴포넌트는 수정을 하지 않는다

새로운 label 컴포넌트를 생성해서 다시 조립해주는것뿐

 

후기

디자인 패턴을 통해 지겨운 복사붙혀넣기 개발에서 벗어나 

좀더 유지보수성이 높은 코드를 구현하고 실제 개발도 간단하게 이미 개발한 부품을 가져와서 다시 조립만 하면 되니 

개발업무도 많이 줄일 수 있었다

물론 모든 개발 상황에서 컴포지션 패턴만이 전부가 아니다

오히려 어떤 상황에서는 컴포지션 패턴이 더 독이 될수 있다 합쳐야하는 부품들이 너무 많아지면 컴포넌트 네이밍도 어려워지고 

해당 컴포넌트들을 구조를 파악하기 어려운 단점이 존재한다.

다양한 디자인 패턴들을 익혀 적재적소에 사용하는것이 중요하다고 생각이든다.