본문 바로가기

Frontned Development/FE 회고

컴포넌트는 어떻게 설계해야할까?

컴포넌트를 어떻게 설계해야할까?

 

요즘 최대 고민이고, 이에 대한 내 의견을 적어두려합니다.

 

자문자답인 만큼 더 많은 경험에 의해서 바뀔 수 있으며, 중간 정답으로 봐주시면 좋을 거 같습니다.

 

글을 읽고 피드백을 해주신다면, 정말 감사합니다.

 

컴포넌트란?

컴포넌트는 화면의 구성요소를 의미한다. 작은 단위로는 button, input 등이 있을 거고 점점 커져가서 dropdown, header, carousel, 화면 그 자체 등 화면에 보이는 거의 모든 요소가 컴포넌트가 될 수 있습니다.

https://atomicdesign.bradfrost.com/chapter-2/#the-part-and-the-whole

 

 

그러면 컴포넌트는 왜 나누는 걸까요?

보통은 "복잡성을 줄이기 위해서"과 "반복되는 코드를 줄이기 위해서 (재사용성을 통해 생산성을 높이기 위해서)" 나누게 됩니다.

 

1. 복잡성을 줄이기 위해서

구글 코드가 20억줄이라 하는데, 이 코드가 모두 한 파일에 있다 생각해봅시다.

 

여기서, 구글의 헤더 부분 관련 코드를 찾는걸 생각해보면... 매우 끔직하네요.

하지만, 요즘 코드 구조처럼 components > organisms > header.tsx 이런 식으로 나누어져있다면, 굉장히 찾기 쉬울 것이다.

이런 식으로 복잡성을 줄이기 위해 사용하며, 복잡성이 적다는 것은 유지보수하기가 편하다는 것을 의미합니다.

 

2. 재사용성을 높이기 위해서

<button css={{marginLeft: "1px"}}>
	버튼
</button>

이런 간단한 코드가 있고, "설문조사 페이지" , "메인 페이지" 등 20곳에서 같은 내용이 필요하다 가정해봅시다.

이 때, 이 코드를 매 페이지 복사해서 붙여넣는 것은 생각보다 매우매우 불편한 노가다 작업이 될 것입니다.

 

더 끔직한 상황을 가정해봅시다.

불편함을 느끼고 작업했지만, 디자인이 별로여서 "marginLeft : 2px"로 수정해야한다는 지시를 받았다. 그러면 각 페이지를 모두 찾아가서 20곳이 바꾸는 작업을 해야할 것이다.

만약, 프로젝트 규모가 큰 내용이라 4000번을 넘게 사용했다면..... 정말 끔찍한 노가다 될 것이다....

 

<MarginButton />

이런 문제를 해결하기 위해서, 컴포넌트라는 재사용 가능한 요소로 묶는 것이다. 해당 요소가 필요하면, 이 컴포넌트를 가져다 쓰면 되고 하나의 파일을 수정하면 사용하는 모든 곳을 동시에 수정할 수 있다.

 

이런 두 가지 장점때문에, 우리는 컴포넌트를 만들게 된다.

 

나누는 이유는 알겠는데, 어떻게 나눠야할까요?

여기서부터가 제 고민이었습니다. 나눠야하는 필요성은 알았어요. 근데, 나누는 기준이 애매했고, 그에 따라서 장점이 약해지고 단점이 부각되는 것 같아요.

아래는 제가 실제로 겪은 고민들이에요

 

1. 무작정 나눠버리니, 파일이 너무 많아지고 찾기가 힘들었어요. 어디까지, 어떻게 나눠야 효율적일까요?

2. 미세한 디자인 차이 때문에, 재사용하기가 너무 힘들었어요. 아래 6개는 모두 다른 컴포넌트일까요? 모두 같은 컴포넌트일까요? 

https://mesign.tistory.com/45

3. 완전히 동일한 UI인데, 기능이 미세하게 달라서 재사용할 수가 없었어요. 왼쪽은 여러 개를 고를 수 있는 MultiRadioButtonList이고, 오른쪽은 하나만 고를 수 있는 단일 RadioButtonList에요. 분명 똑같은 RadioButton을 나열한 리스트인데 이 둘은 다른 컴포넌트일까요?

 

https://brunch.co.kr/@potatohands/244

4. 기능이 같은데, 구성요소(하위 컴포넌트)가 달라요. 이 둘은 같은 컴포넌트가 될 수 있을까요? (즉, 하위컴포넌트와 상위컴포넌트의 의존성이 생겼어요. 그래서, 첫 번째는 RadioButton을 수직으로 나열한 RadioButtonList가 되고, 두 번째는 RightSquartRadioButton 을 수평으로 나열하여 RightSquartRadioButtonList가 됩니다.)

https://blog.naver.com/heartflow89/221164211964

 

실제로, 저의 팀원분께서 제 컴포넌트를 사용하려하는데, 2번, 3번과 같은 미세한 차이가 있어요. 그래서, 제 컴포넌트를 수정하여서 사용하셨는데, 컴포넌트를 사용중인 원래 페이지에서 문제가 발생했어요.

 

고민하다가 결국, 따로 만들었고 그로 인한 단점을 확인할 수 있었어요.

- 하나의 프로젝트에서 비슷한 이름을 지닌 컴포넌트가 여러 개 생겼고, 매 번 파일을 찾을 때, 혼란을 불러와요

- 수정을 하면 기존 화면들에 영향을 주기 때문에, 함부로 수정할 수가 없어요. 즉 애매하게 만들면, 재사용성을 높일 수가 없었어요.

 

어떻게 하면, 이런 단점을 최소화하면서 컴포넌트를 나눌 수 있을까요? 특히, 재사용성을 최대화할 수 있는 방법을 무엇이 있을까요? 이를 나눌 수 있는 기준이 뭐가 있을까요?

 

현재 제가 가지고 있는 고민이에요.

 

제가 찾은 정답은??

1. 무작정 나눠버리니, 파일이 너무 많아지고 찾기가 힘들었어요. 어디까지, 얼만큼 나눠야 효율적일까요?

=> tag 단위부터 bottom-up 방식으로, 기능을 기준으로 컴포넌트를 설계하면 나누는 기준이 명확해지는 것 같아요!

+). 파일이 찾기 어려운 건, 이름이 애매해서 그런 것도 있는 거 같아요. 이름을 매우매우매우 직관적으로 지어봅시다.

 

2. 미세한 디자인 차이 때문에 사용하기가 힘들었어요. 이런 차이가 있으면 같은 컴포넌트일까요? 다른 컴포넌트일까요?

=> 디자인보다 기능을 중심으로 보는 게 맞습니다. 즉, 디자인의 미세한 차이가 있는 대다수의 경우 같은 컴포넌트라 보는 게 맞습니다.(일부 특수한 경우나 내부 구성요소가 다른 경우에는 다른 컴포넌트로 보기도 하는 거 같습니다.)

 

3. 완전히 동일한 UI인데, 기능이 미세하게 달라서 재사용할 수가 없었어요. 이 둘은 다른 컴포넌트일까요? 같은 컴포넌트일까요?

=> "기능이 다르다"는 다른 컴포넌트입니다.

- 라디오 버튼 : 현재 버튼이 true/false를 계산한다.

- 멀티라디오버튼리스트 : 라디오 버튼을 나열한다. 버튼을 클릭 시, true로 만든다.

- 싱글라디오버튼리스트 : 라디오 버튼을 나열한다. 버튼을 클릭 시, true로 만들고, 나머지 버튼을 false로 한다.

하지만, 이 경우는 기능은 똑같고, 로직이 다르기 때문에 같은 컴포넌트입니다.

 

다만, 미세한 차이의 경우 옵션을 추가하거나 확장 가능하게 만들어서 양쪽을 모두 커버 가능하도록 만들면 좋을 거 같습니다.

 

※ 헷갈리실까봐, 이 글에서 로직과 기능에 대한 설명을 서술하겠습니다. 본 내용은 공식적인 내용이 아닙니다.

- 기능 : UI적인 상호작용에 따라서 예측되는 반응 또는 결과를 의미합니다. ex). 버튼은 클릭하면 로직을 실행한다. 특정 화면이 렌더링(나열)된다.

- 로직 : 기능에 따라서 실행되는 구체적인 프로그래밍 과정을 의미합니다. ex). backend로 api요청을 보낸다. 상태를 2로 바꾼다. 순서를 바꾼다.

즉, 기능은 컴포넌트의 목적이 되고, 로직은 컴포넌트의 목적을 이루기 위한 구체적인 방법이 됩니다.

 

4. 기능이 같은데, 구성요소(하위 컴포넌트)가 달라요. 이 둘은 같은 컴포넌트가 될 수 있을까요?

=> 상위 컴포넌트의 기능이 동일하다면, 하위 컴포넌트의 의존성을 없앰으로써 같은 컴포넌트가 될 수 있습니다. 즉, 같은 컴포넌트입니다.

 

그리고 이런 정답들을 만든 기준은 다음 세 가지입니다.

- 컴포넌트는 기능(목표)을 단위로 분리할 것.

- 로직과 UI를 분리할 것.

- 컴포넌트 간 의존성을 최소화할 것

 

이를 좀 더 자세히 설명드리겠습니다.

 

1. tag 단위부터 bottom-up 방식으로, 기능을 기준으로 컴포넌트을 설계하면 나누는 기준이 명확해지고 파일이 적어지는 거 같아요!

저는 여태까지 top-down 방식으로 화면을 많이 만들었습니다. 위 그림처럼 top-down으로 접근하면, 저는 UI를 기준으로 나누는 버릇이 있었습니다. (지금도 파란색 글씨(보라색 네모)와 그림 + 글씨 (초록색 네모)를 공통된 요소로 나누어서 컴포넌트를 만들었네요.)

 

큰 단위에서 접근하다보니 중첩된 기능의 분리가 어려워서 UI단위로 접근을 했던 것 같습니다. 하지만, 기능이 아닌 UI를 기준으로 컴포넌트를 나눈다면, 재활용이 힘든 컴포넌트가 너무 많이 생성되는 단점이 있습니다, (2번에서 상세 설명합니다.)

 

또한, 이번에는 일부러 제외했지만, 노란선으로 한 번 더 세밀하게 나눌 수 있습니다. 너무 위에서 쪼개다보니 가끔씩 빠트리는 부분이 생기는데 이런 빠트린 부분때문에, 설계를 다시할 때도 있었습니다.

 

위 그림은 Atomic Design Pattern으로 bottom-up 방식의 설계입니다. Atom(html tag와 거의 유사, tag는 최소 기능 단위가 됩니다.)을 쌓아서 화면을 만들어 나갑니다.

이 그림에서 atoms(아이콘, 텍스트)들이 모여서 molecules(게시글 제목, 이미지, 게시글 메뉴, 좋아요 버튼)이 되고 다시 이들이 모여서 organisms(게시글), organisms들이 모여서 화면이 됩니다.

 

이렇게 기능의 최소단위부터 설계하여 화면을 만들어나간다면, 기능이 매우매우 단순해집니다. 단순하기 때문에, "하나의 컴포넌트는 하나의 기능(책임)만을 담당하도록" 설계하는 것이 매우 쉬워지고, 이미 있는 기능은 해당 기능을 담당하는 컴포넌트는 재활용하면 되기 때문에 설계가 매우 단순하면서 탄탄하게 세워집니다.

 

이런 점에서, 저는 bottom-up 방식으로, 기능 단위로 설계하는 것은 매우 명확한 컴포넌트 설계 기준이 된다 생각합니다.

 

※ 하지만, atomic 패턴은 장/단점이 뚜렷하므로 이 방식이 좋은 프로젝트 설계는 아닌 거 같습니다. 하지만, 핵심 철학인 컴포넌트 설계의 정답이라 생각하고 있습니다.

 

2. 디자인이 아닌 기능단위로 컴포넌트를 설계해야하는 이유

https://mesign.tistory.com/45

예시를 보면 바로 이해될 거 같습니다.

button이 하나임에도, n개의 컴포넌트가 생기게 됩니다.

ex). Button , HalfBorderRadiusButton, FullBorderRadiusButton, GreenButton, RedButton, GreenHalfBorderRadiusButton ........

 

버튼 하나에서만 이렇게 예시가 나오는데, 매우 끔직합니다. 프로젝트가 버튼으로 뒤덮일 수도 있어요. 그래서, 우리는 기능단위로 설계를 해야합니다. 

 

그러면, 위처럼 디자인이 다른 경우는 어떻게 해결해야할까요? Props로 style의 일부를 변경할 수 있도록 설계해주면 됩니다.

<Button borderRadius="16px" color="#ff7f00" />

 

 

이런 식으로 표현하면, Button 하나라도 여러 버튼을 표현할 수 있죠. 다만, 이 경우는 재사용성이 떨어집니다. 이 문제는 어떻게 해결할 수 있을까요? 제한을 하면 됩니다.

// large = 20px, medium = 16px, small = 12px
// orange = "#ff7f00"

<Button borderRadius="large" color="orange" />
<Button type="orange" />

이런 식으로 프로젝트에서 사용하는 디자인을 미리 정해둔다면, 통일된 디자인으로 재사용성을 극대화할 수 있습니다. 전체적인 수정이 필요할 경우, 통일 디자인을 정의한 부분만 수정하면 됩니다.

 

 

 

이 경우도 기능은 동일하기 때문에, 같은 컴포넌트입니다. 실제로, MUI나 Goolge Material Design 3에서도 같은 컴포넌트로 취급하고 있습니다.

 

MUI는 Icon Button의 경우 기능이 확장되어서, Icon Button만 Button을 확장한 단일 폴더/파일로 존재합니다. Material Design은 기본이 되는 Button Component를 상속하여서 css style만 덮어씌워 만든 파일들이 다수 존재합니다. 다만, style만 바뀐 것이고, 기본 베이스 컴포넌트는 하나입니다.

 

위 라이브러리 예시처럼, 재사용성을 높여야할 경우, 파일을 나누고 세분화하는 것도 좋을 것 같습니다. 하지만, 기본적으로는 같은 컴포넌트입니다.

 

※ 혹시, "버튼도 기능이 다 다른거 아닌가?" 하는 고민을 하실까봐 남깁니다. 버튼의 기능은 "눌렀을 때, 로직을 실행한다" 입니다. 그래서 다양한 로직을 실행할 수 있지만, 기능은 하나입니다.

const onClickHandler = () => {
	console.log("버튼의 기능은 어떤 로직을 실행하는 것이다!")
}

<Button onClick={onClickHandler}/>
function Button({onClickHandler}) {
	<div onClick={onClickHandler}>button</div>
}

 

 

3. 기능이 다르면, 다른 컴포넌트다. 다만, 로직이 다르고 기능이 같으면 같은 컴포넌트다.

이미, 2번에서 설명했듯이 기능단위로 분류하기 때문에 기능이 다르면 다른 컴포넌트로 설계하는 것이 맞습니다. 다만, 비즈니스 로직의 차이만 있을 경우는 같은 기능이 될 것입니다.

 

라디오버튼리스트의 기능은 "내부 요소를 나열하고, 정답을 가진다"가 될 것입니다.

멀티라디오버튼리스트나 싱글라디오버튼리스트나 똑같습니다.

단지, 로직이 다릅니다. 괄호는 라디오 버튼 리스트의 로직이며, 괄호밖은 라디오버튼 리스트의 로직입니다.

 

- 멀티라디오버튼리스트 : (선택지를 클릭 시, true로 상태를 변경합니다. 재클릭 시, false로 상태를 변경합니다.) 모든 true 선택지를 정답으로 기록하고 있습니다.

- 싱글라디오버튼리스트 : (선택지를 클릭 시, true로 상태를 변경합니다.) 다른 선택지는 모두 false로 변경합니다. 현재 true인 선택지를 정답으로 기록하고 있습니다.

 

그래서, 두 개는 같은 컴포넌트가 맞습니다.

 

4. 기능이 같으면, 하위 컴포넌트가 달라도 같은 컴포넌트다.

사실, 이 부분을 해결을 못하고 있었습니다.기능이 같은데, 하위가 다르면 다르게 만들 수 밖에 없었다. 다행히도, 우리 팀원분께서 한 링크를 공유해주셨고, 여기서 막막했던 부분이 싹 해결되었다. 키워드는 의존성이었다.

 

 

복잡한 컴포넌트 유연하게 설계하기

프론트엔드 개발자의 일을 하다보면 복잡한 요구사항의 컴포넌트를 제작할 일이 있습니다. 복잡한 컴포넌트라는게 무엇인지 생각을 해본다면 서버에서 내려준 데이터를 UI로 표현하는게 복잡

velog.io

const NextedCombobox = () => (
  <Combobox>
    {items.map((item) => <CheckboxItem item={item} >)}
  </Combobox>
)

const NextedCombobox = (renderItem) => (
  <Combobox>
    {items.map((item) => renderItem(item))}
  </Combobox>
)

저는 항상 첫 번째 방식으로 작성하였고, type 이나 store를 연결해두다 보니 항상 의존성이 생겨서 상위 컴포넌트들을 재활용할 수 없었습니다. 하지만, 이 글에 작성자분은 두 번째 방식으로 작성하시고, type과 로직들은 분리하여 이런 의존성을 최소화하셨고 컴포넌트를 재활용가능하게 설계를 바꾸셨다. 

 

이런 과정을 거쳐서 1-4번 질문에 대한 답이 나왔고, 다음과 같이 정리되었다.

- 컴포넌트는 기능(목표)을 단위로 분리할 것. (1번, 2번)

- 로직과 UI를 분리할 것. (2번, 3번, 4번)

- 컴포넌트 간 의존성을 최소화할 것 (4번)

 

또, 여기서 다루지는 못했지만, 디자인 시스템이 이런 내용을 완벽히 다룬 정답인 것 같다. 이 또한, 나중에 기회가 된다면 설명해보겠다.

 

긴 자문자답이 끝났다. 다음에는 코드를 가지고 이런 결과를 증명해보는 시간을 가져봐야겠다.

 

항상 질문과 피드백은 환영입니다.

참고자료

 

아토믹 디자인시스템 : https://atomicdesign.bradfrost.com/table-of-contents/

컴포넌트 의존성 분리에 관한 블로그글 : https://velog.io/@moreso/designing-complex-components-flexibly

git, material-UI : https://github.com/mui/material-ui/blob/master/packages/mui-material/src/Button/Button.js

git, mui : https://github.com/mui/material-ui

카카오 기술블로그, 컴포넌트와 추상화 : https://fe-developers.kakaoent.com/2022/221020-component-abstraction/