React 컴포넌트의 역할에 대한 생각

2020년 10월 14일 · #에세이


글이 길다.

리액트로 4개의 큰 프로젝트와 여러개의 작은 프로젝트를 진행하면서 컴포넌트를 어떻게 정리해야 하는가 에 대한 많은 생각을 하게 된다. 이 생각은 지금도 진행중이고 앞으로도 진행 될 것이다. 그 생각을 여기 정리해본다.

  • Container / Presenter 패턴
  • Atomic Design System

위 두가지 패턴을 적용해 본 경험이 있다. Container / Presenter 패턴은 리액트가 탄생하면서 아주 많이 쓰인 패턴인데, Hooks 로 (거의) 모든 게 가능한 지금 시점에서는 위 패턴은 사용하지 않아도 된다는게 학계의 정설(?)이다. 일단 위 패턴을 만드신 분이 그렇다고 말했고, 실제로 굳이 사용하지 않아도 코드를 깔끔하게 짤 수 있기도 하다.

Atomic Design System 은 사실 리액트를 겨냥해서 만들어진 패턴은 아니다. 하지만 리액트와 꽤 잘 어울린다고 생각했다. 나는 이 패턴을 어떻게든 효과적으로 적용해보려고 많은 시도와 시행착오를 겪었다. (지금도 겪고있다.)

처음 리액트를 배우며 간단한 예제들을 만들때는 위 패턴들은 물론이고 굳이 패턴을 적용하지 않고 마음대로 컴포넌트를 만들어도 전혀 문제가 되지 않다. 깔끔하다. 양이 적기 때문에. 하지만 컴포넌트 파일이 수백개가 되고, 페이지도 수십 페이지가 되고, 수많은 복잡한 비즈니스 로직들과 조건문들과 기능들과 예외적인 상황들과 많은 API들이 붙으면서 앱은 복잡해지면서 많은 버그들이 생겨나게 된다.

개발자와 버그는 어쩔 수 없이 공존해야 하는 운명이라 했다. 그것까진 좋다. 하지만 우리는 빠르게 버그를 발견하고 처리하기 위해서는 앱은 유지보수가 용이해야 하며 깔끔하고 이해하기 쉬운 코드와 구조로 되어있어야 한다.

그러한 구조를 만들기 위해 내가 적용중인 패턴을 설명해보고자 한다.

여러 프로젝트를 진행하면서 필자가 필요하다고 느낀 리액트 컴포넌트에 역할에 대해서 먼저 설명해야한다. 이 글의 제목이기도 하다.

  1. User Interfaces
  2. User Interfaces Groups
  3. Layouts
  4. Actions
  5. Modules

필자는 위 네 종류 역할의 컴포넌트가 필요하다고 느꼈다. 물론 Pages 라는 역할도 분류돼야 하지만 여기서는 제외했다. 하나 하나 뜯어보자.

누구나 많이 들어본 말일 것이다. 이 컴포넌트는 Container / Presenter 패턴으로 보자면, Presenter(Dumb or Stateless) 컴포넌트에 속할 것이며, Atomic Design System 으로 보자면 Atoms 에 속할 것이다.

이 컴포넌트는 아주 작은 단위의 디자인을 담당한다. 보통은 State 를 가지고 있지 않는다. 이 단위의 컴포넌트의 특징을 알아보자.

  • 재사용성 매우 높음
  • Stateless (Props에 의존)
  • 디자인 담당
  • Sementic (의미적) 으로 분류

이러한 특징들을 가지고 있어야 한다. 재활용성이 매우 높기 때문에, 심지어 다른 프로젝트에 갖다 놔도 사용할 수 있어야 한다. 디자인만 살짝 바꿔서 사용할 수 있을 것이다. 그렇다면 어떤 컴포넌트들이 이 역할에 해당할까?

  • Buttons
  • Inputs
  • Texts
  • Logo
  • Images
  • Cards
  • Lists
    ...

위처럼 작은 단위의 컴포넌트들을 포함한다. 이 컴포넌트들은 Props에 의존하고 있기 때문에, 버튼을 예로 들면 버튼에 포함 되는 texticoncolorsizealign 등 모두 props 로 내려받는다. 물론 기본값도 있어야 할 것이다.

이 컴포넌트는 프로젝트 전반적으로 매우 많이 사용될 것이다.

이 컴포넌트는 말 그대로 User Interface 컴포넌트들을 모아놓은 그룹이다. Atomic Design System 으로 보자면 Molecules 에 속할 것이다. 이 컴포넌트의 특징을 알아보자.

  • 재사용성 높음
  • Stateless (Props에 의존)
  • 디자인 담당

위 User Interfaces 컴포넌트들은 어디에나, 심지어 다른 프로젝트에서도 사용할 수 있을거라고 설명했다. 하지만 이 컴포넌트들은 현재 프로젝트를 위해 존재한다. (물론 다른 프로젝트에 적용이 불가능하진 않다.)

Atomic Design System의 예시를 보면 Label + Input 즉, 라벨이 씌여진 입력필드가 Molecules 로 분류된다. 하지만 필자는 리액트에선 그렇게 사용하지 않는다. Input 에 Label 이 필요하다면 label='My Label' 과 같은 Props를 만들면 되기 때문이다. Atom 에서 다 해결된다.

그렇다면 어떤 컴포넌트들이 이 역할에 해당될까?

  • 유저 정보 (ex: Avatar + Username + Email)
  • 댓글 아이템 (ex: Username + Comment + CreatedAt)
    ...

이 컴포넌트는 보통 다른 컴포넌트들을 감싸는 용도로 사용한다. 즉 children props 를 반드시 사용하게 될 것이다. 이 컴포넌트의 특징을 알아보자.

  • 재사용성 높음
  • Stateless
  • 자주 사용되는 레이아웃
  • Sementic (의미적) 으로 분류

그렇다면 어떤 컴포넌트들이 이 역할에 해당될까?

  • Content Wrapper (ex: max-width 768px 의 글 너비)
  • Grid System
  • Table
  • Inline Item (ex: 많아지면 좌우 스크롤되는 아이템들)

이 컴포넌트들의 역할에는 드디어 기능 이란게 들어간다.

기능들은 Modules 에 대부분 있겠지만, 앱을 만들다보면 분명 작은 단위로 재활용되어야 하는 기능들이 존재한다. 이 역할은 그러한 컴포넌트들을 말한다. 이 역할은 Container / Presenter 패턴이나 Atomic Design System 어디에도 존재하지 않는 것 같다. 이러한 컴포넌트들 때문에 많은 고민이 필요했고 이렇게 정리하게 된 것이다. 굳이 찾아보자면 Atoms 에 포함되는 것 같긴 하지만 Atoms 가 기능까지 담당해버리면 재사용성이 무너지게 된다.

나는 Atomic Design System 을 적용하고 있는 프로젝트에서 이 컴포넌트 역할을 Elements 로 분류했다. 이 컴포넌트의 특징을 알아보자.

  • 프로젝트내에서 재사용 가능함
  • 기능 담당
  • 어디에 배치하든 혼자서 완벽한 기능을 해내야 함.
  • props를 받지 않아도 됨.

그렇다면 어떤 컴포넌트들이 이 역할에 해당될까?

  • 테마 변경 버튼
  • 언어 선택 버튼 (또는 드롭다운 메뉴)
  • 국가코드 선택 드롭다운 메뉴
    ...

의도를 눈치 챘는가? 기능을 포함한 User Interfaces 컴포넌트라고 말할 수 있다. 버튼이든 드롭다운이든 User Interfaces 역할의 컴포넌트들을 렌더 해야 할 것이다.

언어 선택 버튼을 예로 보면, 언어를 바꿀 때 마다 언어를 바꾸는 함수를 실행하게 될 것이다. (ex: i18n.changeLanguage(id))
또 국가 코드 리스트 드롭다운 메뉴를 예로 보면, 부모 컴포넌트로부터 onChange 와 같은 함수를 props 로 받아서 국가 코드를 바꿀 때 마다, 바꾼 언어의 id 값 등을 부모 컴포넌트로 전달 해 줘야 할 것이다. Submit은 부모의 역할이다.

이 컴포넌트는 아마도 fetch API 를 사용하게 될지도 모른다. (or Axios) 언어 목록이나 국가코드등이 DB 로 관리되고 있거나, 필요한 리스트들을 API 로 요청해야 할지도 모르기 때문이다.

Modules는 데이터와 기능을 모두 포함하고 있으며 매우 독립적으로 작동해야한다. 조건이 분기되기도 하고 데이터를 요청해 받아오기도 한다. 직접 Fetch(Axios) 한 데이터를 State로 가지고 있기도 한다.

Atomic Design System 으로 보자면 Organisms 에 속한다고 생각한다. 하지만 많이 다르다. Atomic Design System의 Organisms는 아직 데이터들을 받지 않은 상태이다. 데이터는 Template를 통해 Pages 로 받는다. 처음에는 그렇게 구조를 짜봤지만 코드가 너무 복잡하고 지저분해졌다.

이 컴포넌트의 특징을 알아보자.

  • 재사용성 낮음
  • 데이터 로드 / 가공
  • 기능 별 분류
  • 조건 분기
  • 완벽히 독립적으로 작동

그렇다면 어떤 컴포넌트들이 이 역할에 해당될까?

  • 회원가입/로그인 모듈
  • 피드 리스트
  • 팔로워 리스트

예시중에 팔로워 리스트와 같은 경우는 타겟 유저의 user id 를 props 로 내려받아 해당 유저의 followers 리스트를 api 로 요청하게 될 것이다. user id 가 undefined 라면, current user의 id를 Redux/MobX 등에서 가져와서 api를 요청할 수도 있다.
피드 리스트의 경우 current user의 id 만 필요하기 때문에 아마 props 를 받지 않아도 될 것이다.

위 설명에서 Pages 는 제외했지만, 간단히 역할을 설명하자면 Buiness Logic에 따라 Modules 컴포넌트들을 배치하는 역할을 한다. 전역적인 데이터를 관리하기도 할 것이다. react-router-dom의 컴포넌트로 렌더되는 가장 첫번째 컴포넌트가 될 것이다.

어떻게 하면 편리하고 효율적으로 깔끔하게 유지보수 할 수 있는 구조를 만들까 하는 고민은 앞으로 계속 하게 될 것이다. 이렇게 정리 해 놓은 상황에서도 예외 사항은 분명 생길것이고 더 좋은 아이디어를 가지고 있는 사람들도 만나게 될 것이다.

누군가 나와 비슷한 고민을 하고 있는 개발자가 이 글을 보게 된다면 내 생각들이 도움이 되었으면 좋겠다.