Navigation

컴포넌트로 나눌 때 감출 책임

Culturepass 구축을 준비하던 2025년 5월에 컴포넌트를 어디까지 나눌지 글로 정리했습니다. 같은 카드의 클래스와 DOM 구조를 기억해서 조립하는 방식이 반복됐고 반대로 단순한 제목까지 컴포넌트로 감싸는 경우도 고민했습니다.

원문은 분리 기준을 제안한 기록입니다. 문서의 모든 기준이 팀 표준이 됐다고 확인한 것은 아닙니다. 이번에는 원문의 체크리스트를 다시 읽고 스타일을 재사용하는 문제와 UI의 책임을 감추는 문제를 나눠 설명했습니다.

같은 모양이어도 같은 책임은 아닙니다

여백이나 글자 크기가 같은 두 영역이 서로 다른 이유로 바뀔 수 있습니다. 제목 스타일을 공유하려고 전체 영역을 하나의 컴포넌트에 넣으면 한쪽에만 필요한 버튼과 상태가 props로 계속 늘어날 수 있습니다.

반대로 DOM 순서, 버튼 의미와 로딩 상태까지 같은 카드라면 클래스 조합을 호출부마다 반복하는 쪽이 실수를 만들기 쉽습니다. 이때는 컴포넌트가 올바른 구조와 상태 조합을 책임지도록 할 수 있습니다.

확인할 질문스타일만 공유하는 경우컴포넌트를 검토할 경우
무엇이 반복되는가?색, 간격, 글꼴의미가 있는 DOM 구조와 동작
호출부가 기억할 규칙은?클래스 한두 개요소 순서, 접근성, 상태 조합
변경 이유가 같은가?화면마다 다름같은 UI 계약을 따라 함께 변경
상태를 누가 처리하는가?상태가 없거나 부모가 모두 처리로딩, 선택, 펼침 같은 경계가 있음

중첩이 많거나 사용 횟수가 많다는 것만으로 결정하지는 않습니다. 한 번 쓰는 UI도 독립된 책임이 크면 나눌 수 있고 여러 번 쓰는 단순 스타일은 CSS로 충분할 수 있습니다.

호출부에서 없앨 지식을 찾았습니다

예를 들어 혜택 카드가 제목, 이미지와 상태 배지를 보여 준다면 호출부가 배지의 DOM 순서까지 알 필요는 없습니다. 하지만 신청 API와 인증 화면 이동까지 카드 안에 넣으면 카드가 특정 화면의 업무 정책을 알아야 합니다.

다음은 경계를 설명하려고 다시 작성한 예시입니다. 실제 서비스 코드를 발췌한 것은 아닙니다.

업무 정책을 props 계약 밖에 두기tsx
type BenefitCardProps = {
  title: string
  imageUrl: string
  statusLabel: string
  onOpen: () => void
}

const BenefitCard = ({ title, imageUrl, statusLabel, onOpen }: BenefitCardProps) => (
  <article>
    <img src={imageUrl} alt="" />
    <h3><button type="button" onClick={onOpen}>{title}</button></h3>
    <span>{statusLabel}</span>
  </article>
)

카드는 표현 구조와 버튼의 의미를 맡고 호출부가 항목 id와 이동 정책을 연결합니다. 여러 화면에서 상태 라벨 정책까지 같다면 그 규칙을 ViewModel 변환으로 모을 수 있습니다. 이 구분은 응답과 화면 로직을 나누는 흐름과 이어집니다.

flowchart LR
    Screen["화면: 인증, 요청과 이동"] --> VM["ViewModel: 표시 값"]
    VM --> Card["컴포넌트: 구조와 상호작용 계약"]
    Style["CSS: 색, 간격과 배치"] --> Card
    Card -->|onOpen| Screen

로딩 화면도 경계를 드러냈습니다

Culturepass의 홈 배너에는 화면 내부 skeleton 컴포넌트를 따로 둔 구현이 있었습니다. 로딩 중의 DOM과 표시 구조를 한곳에서 읽을 수 있다는 점은 원문의 분리 기준과 맞았습니다. 그렇다고 skeleton 하나의 존재로 전체 설계 원칙이 모두 적용됐다고 볼 수는 없습니다.

로딩과 정상 상태를 나눌 때는 바깥 크기가 지나치게 달라지는지, 보조 기술에 같은 콘텐츠가 중복 노출되는지, 클릭할 수 없는 영역이 버튼으로 남는지도 확인해야 합니다. 파일을 나누는 것만으로 UI 계약이 완성되지는 않습니다.

추상화를 유지할 비용도 봅니다

컴포넌트를 만들면 클래스와 DOM 지식 일부를 감출 수 있습니다. 그 대신 props 계약과 변형 규칙을 유지해야 합니다. compact, special, alternate 같은 옵션이 서로 충돌하기 시작하면 같은 책임을 묶었는지 다시 살핍니다.

분리 여부를 결정할 때 마지막으로 확인하는 것은 호출부가 얼마나 짧아졌느냐보다 호출부에서 어떤 규칙을 몰라도 되게 됐느냐입니다. 감춘 규칙이 분명하면 컴포넌트를 유지할 이유도 설명하기 쉬웠습니다.