다짐) 디자인 시스템 설계에 대한 고민들 (Part 2. 분리와 결합)

2ast·2023년 8월 19일
0

컴포넌트의 분리와 결합

비슷한 컴포넌트를 만들 때 분리해서 각각 구현할 것인지, 아니면 하나의 컴포넌트로 구현할 것인지 결정하는 일은 얼핏 쉬운 일처럼 보여도 꽤나 자주 나를 고뇌에 빠뜨렸던 주제였다.

다짐의 Button 컴포넌트와 Text Card 컴포넌트를 가져와봤다. 두 컴포넌트는 variation을 살펴봐도 꽤나 비슷한 점이 많은데, props를 이리저리 잘 만지면 충분히 상호 전환이 가능해 보인다.(마치 위상수학같다.) '비슷한 점이 상당히 많다면 하나의 컴포넌트로 빼서 코드 중복을 줄이는 편이 좋은 프로그래밍이 아닌가?' 하는 생각이 스치지 않았는가?

이번에는 Text 컴포넌트를 생각해보자. 이전 프로젝트를 돌아보면 Text 컴포넌트를 매번 필요할 때마다 정의해서 사용하곤 했다.

const Title = styled.Text`
	font-size: 20px;
	...
`
const Description = styled.Text`
	font-size: 16px;
	...
`

그렇다면 디자인 시스템도 이런식으로 각각 정의하는 방향이 맞는 걸까? 아니면 하나의 컴포넌트로 제작해서 props로 핸들링하는게 맞는 걸까?

<TextTitle20Bold></TextTitle20Bold>
<TextBody14Regular></TextBody14Regular>
<DgText fontType={'title_20_bold'}></DgText>
<DgText fontType={'body_12_regular'}></DgText>

목적 중심 판단

나는 목적성을 기준으로 컴포넌트의 분리와 결합을 결정한다. 디자인 관점에서 비슷하게 생겼다고 하더라도, 두 컴포넌트의 쓰임 목적이 전혀 다른 경우가 있을 수 있다. 바로 직전에 다뤘던 Button과 Text Card의 관계가 그러하다. 반면 Text 컴포넌트들은 그 목적이 서로 동일하다고 볼 수 있다. '목적이 일치하는가?'라는 질문을 조금 더 구체적으로 풀어보면 '동일 스크린에서 상호 전환될 가능성이 있는가?'로 바꿔볼 수 있으며, 컴포넌트의 분리와 결합을 코드 관점에서 바라보면 컴포넌트를 이름으로 구분할 것인가 props로 구분할 것인가를 결정하는 것이기도 하다. 이런 관점의 질문들이 어떻게 컴포넌트 설계의 근거가 되는지 하나씩 살펴볼 것이다.

//조건에 따라 Button과 Text Card가 혼용될 가능성은 거의 0에 수렴한다.
condition ? <DgButton/> : <TextCard/> 
<DgTextCardButton type={condition ? 'button' : 'card'} {...props}/>

목적이 같은 컴포넌트 결합의 메리트

쉬운 이해를 돕기위해 Text 컴포넌트를 살펴보겠다. Text 컴포넌트는 variation이 다양하더라도 목적이 동일하므로 컴포넌트 내부에서 충분히 상호 전환이 가능하다.

<DgText fontType={isSelected?'body_12_bold':'body_12_regular'} {...props}>1</DgText>

이때 isSelected라는 조건에 의해 fontType이 전환되는데, 같은 위치에서 같은 목적으로 렌더링되는 컴포넌트이므로, text의 color라든가, style, children 같은 props를 공유할 수 있게된다. 만약 Text를 type에 따라 별개의 컴포넌트로 구분했다면 동일한 props를 중복해서 넣어주어야 했을 것이다.

{isSelected ? 
  <TextBody12Bold {...props}>1</TextBody12Bold> : 
  <TextBody12Regular {...props}>1</TextBody12Regular>
}

//코드 중복을 피하는 몇가지 방법이 있지만 코드가 동떨어짐에 따라 가독성이 떨어지는 것은 어쩔 수 없다.
const TabText = isSelected ? TextBody12Bold : TextBody12Regular
<TabText {...props}>1</TabText>

//개별 속성을 별도 props로 빼면 되는거 아닌가 생각할 수도 있지만
//fontType에 따라 size, weight, lign height, letter spacing 등이 명확하게
//정의되어 있기 때문에 엄격한 디자인 시스템 준수를 위해 이 속성들을 별도로 주는 것을 경계해야한다.
<TextBody size={12} weight={isSelected ? '700':'400'}>1</TextBody12>

이 관점이 시사하는 바는 Text Component를 하나의 컴포넌트로 결합했을 때 추가적으로 얻을 수 있는 메리트는 분명한 반면, Button과 Text Card를 하나의 컴포넌트로 만들었을 때 얻을 수 있는 메리트는 그리 크지 않다는 점이다. 컴포넌트 결합의 메리트를 얻을 수 없다면 내부 구현코드의 복잡성 증가와 가독성 저하를 감수하면서까지 선뜻 컴포넌트를 합치는 결정을 하기가 쉽지 않다. 그럴 바에는 차라리 분리하여 시멘틱하게 가독성이라도 챙기는 것이 낫다는 의견이다.

목적이 다른 컴포넌트 결합의 위험성

또 다른 이유는 목적성이 다른 두 컴포넌트가 지금은 아주 비슷하게 생겼더라도 언제 어떻게 분기하여 달라질지 알지 못한다는 점이다. 겉보기에 아주 비슷하게 생긴 두 컴포넌트를 하나로 결합해 개발하면 중복코드를 크게 줄일 수 있다는 유혹에 빠지기 쉽다. 두 컴포넌트를 props로 구분하는게 불만이라면 '최소한 Base component를 추출해서 재사용하면 장점만을 취하게 되는거 아닐까' 싶을 수 있지만 개인적으로 이 방법도 그다지 추천하고 싶지는 않다.
만약 디자인 시스템이 앞으로 영영 변하는 일이 없고, 적절한 수준에서 Base component를 추출한다면 다행이지만 미래는 불분명하기 때문이다. 목적성이 다른 두 컴포넌트는 변경사항이 생겼을 때 다른쪽의 컴포넌트를 고려하지 않는다. 열심히 코드를 분기처리해서 하나로 구현해놨는데, 갑자기 TextCard와 Button의 variation이 하나씩 추가가 된다고 가정해보자. Button의 변경사항은 TextCard를 고려하지 않았을 것이고, 새롭게 추가된 두 variation이 하나의 코드에 양립하기에는 꽤나 동떨어진 형태라면 그때서야 부랴부랴 두 컴포넌트를 분리하는 작업을 해야할 것이다.
이런방식의 컴포넌트 분리 결정 알고리즘은 디자인 시스템 뿐만 아니라 일반 컴포넌트를 제작할때도 통용된다. 언젠가 유튜브에서 봤던 당근마켓 개발자의 인터뷰를 인용하자면, 상품 목록에 네이티브 광고를 추가하기로 결정됐을 때 이 광고 컴포넌트와 중고물품 컴포넌트는 굉장히 비슷한 형태였지만, 두 컴포넌트의 data 소스와 목적성이 전혀 다르므로 의도적으로 코드 중복을 허용해서 별개의 컴포넌트로 개발했다고 한다. 이처럼 미래가 불확실할 때는 의도적으로 코드 중복을 허용해서 유지보수 용이성을 챙기는 선택이 유리하다.

컴포넌트 분리와 re-mount

마지막으로, 목적성을 기준으로 두는 것은 의미론적 관점뿐만 아니라 React의 성능 관점에서도 유의미하다. react는 조건부 렌더링으로 컴포넌트 전환이 일어날 때 Component name이 다르면 re-render가 아니라 re-mount를 발생시키기 때문이다.

<Card title={title} /> //re-redner

{ title === 'A' ? <Card title={"A"} /> : <Card title={"B"} /> } //re-render

{ title === 'A' ? <Card title={"A"} /> : <Button title={"B"}/> } //re-mount

react는 리렌더 이후 DOM을 비교하는 과정에서 컴포넌트의 이름이 바뀌었는지를 확인하고, 만약 컴포넌트 이름이 바뀌었다면 이전 컴포넌트를 unmount하고 새로운 컴포넌트를 mount하게 된다. react의 rerender는 비용이 크지 않은 반면 remount는 비용이 많이 드는 작업이다. 그러므로 만약 state에 따라 충분히 전환이 가능한 종류의 컴포넌트라면 props를 기준으로 분기하는 것이 성능 관점에서 유리하다. 이 관점의 연장선상으로 컴포넌트 최적화를 진행할 때 무조건 rerender 자체를 최소화하려고 하는 것보다는 비용이 커다란 remount를 찾아서 방지하는 편이 좋다.

예외 - 시맨틱

지금까지 목적성에 기반을 둔 분리 케이스를 말했는데, 사실 한가지 예외케이스가 있다. 바로 시맨틱이 더욱 중요해지는 경우다. 디자인 시스템에 정의된 내용은 아니지만 나는 개발 편의성을 위해 structure component를 디자인 시스템 폴더에 만들어서 사용하고 있다. structure 컴포넌란 화면의 구조를 잡을 때 사용하는 컴포넌트를 말하며, Row, Column, LeftAlignedRow, CenteredColumn 등이 있다.


export const Row = styled.View`
  flex-direction: row;
`;

export const SpacedRow = styled(Row)`
  justify-content: space-between;
  align-items: center;
`;

export const CenteredRow = styled(Row)`
  justify-content: center;
  align-items: center;
`;

export const AlignedRow = styled(Row)`
  align-items: center;
`;

export const RightAlignedRow = styled(AlignedRow)`
  justify-content: flex-end;
`;

export const LeftAlignedRow = styled(AlignedRow)`
  justify-content: flex-start;
`;

아래와 같이 컴포넌트의 구조를 잡을 때 사용한다.

return <Column>
  <SpacedRow>
    <LeftAlignedTouchableRow onPress={onPress}>
      <A/>
      <B/>
    </LeftAlignedTouchableRow>
    <Column>
      <C/>
      <D/>
      <E/>
    </Column>
  </SpacedRow>
  <CenteredRow>
    <F/>
    <G/>
  </CenteredRow>
</Column>

structure 컴포넌트를 목적 기준으로 생각해보면 props로 구분하는게 적절하겠지만, 컴포넌트 구조를 잡는 특성상 여러번 중첩해서 반복적으로 쓰이기 때문에 컴포넌트 이름으로 시맨틱하게 구분해서 가독성을 강조하는 것이 적절하다고 판단했다. 이런 이유로 structure 컴포넌트만은 각각의 특성에 따라 모두 개별 컴포넌트로 선언해서 사용하고 있다.

판단은 각자의 몫

쓰다보니까 조금 길어졌는데, 노파심에 조금 첨언하자면 내가 이런 기준으로 컴포넌트를 나눴다고 해서 이게 정답인 것은 아니다.(당연하다) 아직도 매번 컴포넌트를 만들 때마다 고민 상황을 마주하고 지금까지 작성한 컴포넌트가 최상의 상태라고 생각하지도 않는다. 단지 '디자인 시스템을 구축하는 과정에서 저사람은 어떤 고민을 했고, 나름대로 저런 답을 내렸구나' 정도로 받아들여 주면 좋겠다. 각각의 팀과 프로젝트마다의 상황이 다를 것이니 여러가지 관점에서 깊게 고민해보고 상황에 맞는 판단을 내리길 바란다.

profile
React-Native 개발블로그

0개의 댓글