문제정의

요구사항은 다음 체크박스가 있고, 모두 체크할 시 다음으로 넘어가는 패턴입니다.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/359fb23c-81ad-4819-8c10-e4b98673f549/Untitled.png

현재 2개의 박스만 체크되어있으므로 다음 버튼은 비활성화 되어있죠. 이때, 마지막 check3항목을 체크할 시 다음버튼이 활성화 되게 됩니다.

있는 그대로 구현하면 다음과 같이 되겠죠.

const labels = ['check 1', 'check 2', 'check 3']
 
const App: React.FunctionComponent = () => {
  const [checkList, setCheckList] = useState([false, false, false])
 
  // index 번째 체크 상태를 반전시킨다
  const handleCheckClick = (index: number) => {
    setCheckList((checks) => checks.map((c, i) => (i === index ? !c : c)))
  }
 
  const isAllChecked = checkList.every((x) => x)
 
  return (
    <div>
      <ul>
        {labels.map((label, idx) => (
          <li key={idx}>
            <label>
              <input
                type='checkbox'
                checked={checkList[idx]}
                onClick={() => handleCheckClick(idx)}
              />
              {label}
            </label>
          </li>
        ))}
      </ul>
      <p>
        <button disabled={!isAllChecked}>다음</button>
      </p>
    </div>
  )
}

위 코드를 보면 useState를 이용해 checkList라는 상태변수를 선언했습니다. boolean배열이죠.

코드 기능상 아무런 어려움이 없지만, 컴포넌트를 재사용할 수 있게 하고싶을때가 문제겠죠. 어떻게해야할까요?

일반적인 컴포넌트 분할 방법

보통 컴포넌트를 분할한다면 아래와 같이 Checks 컴포넌트를 만들 수 있겠죠. 이 컴포넌트는 레이블목록인 labels와 현재 check 상태 목록인 checkList, 그리고 체크상태 변경을 담당하는 onCheck핸들러로 구성되어있어요. 코드를 보시죠.

type Props = {
  checkList: readonly boolean[]
  labels: readonly string[]
  onCheck: (index: number) => void
}
 
export const Checks: React.FunctionComponent<Props> = ({
  checkList,
  labels,
  onCheck
}) => {
  return (
    <ul>
      {labels.map((label, idx) => (
        <li key={idx}>
          <label>
            <input
              type='checkbox'
              checked={checkList[idx]}
              onClick={() => onCheck(idx)}
            />
            {label}
          </label>
        </li>
      ))}
    </ul>
  )
}

그럼 이 Checks컴포넌트를 재사용하는 코드는 다음처럼 구성할 수 있습니다.

const labels = ['check 1', 'check 2', 'check 3']
 
const App: React.FunctionComponent = () => {
  const [checkList, setCheckList] = useState([false, false, false])
 
  // index 번째 체크 상태를 반전시킨다
  const handleCheckClick = (index: number) => {
    setCheckList((checks) => checks.map((c, i) => (i === index ? !c : c)))
  }
 
  const isAllChecked = checkList.every((x) => x)
 
  return (
    <div>
      <Checks
        checkList={checkList}
        labels={labels}
        onCheck={handleCheckClick}
      />
      <p>
        <button disabled={!isAllChecked}>다음</button>
      </p>
    </div>
  )
}

이렇게 하면 확실히 컴포넌트를 분할할 수 있겠죠. 하지만 그다지 이상적이진 않습니다. 지금 상황을 도식화해보면 다음과 같으니까요.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/bc16fa18-b7d2-481a-9b64-471b07bd32ec/Untitled.png

원래 App컴포넌트는 많은 구성요소를 가지고있습니다. checkList인 상태변수, 이벤트 핸들러, isAllChecked, 체크박스렌더링 등이 있죠. 이중에서 Checks컴포넌트를 분리하는 과정에서 App에서 순수하게 빠진것은 오로지 체크박스를 그리는 기능 뿐입니다. 나머지 4개의 기능은 여전히 App에 남아있고 재사용할 수 없죠.

특히 CheckList가 여전히 App컴포넌트에 남아있는것은 굉장히 비효율적입니다. App이 다음버튼의 disabled속성을 계산해야하기 때문이죠. 동작은 하나 바람직한 해결책은 아닙니다.