원문: https://andela.com/insights/structuring-your-react-application-atomic-design-principles/
필자는 처음 프로그래밍을 시작했을 때, 디자인 패턴, 파일구조와 같은 추상적인 프로그래밍의 개념과 중요성을 전혀 몰랐다. 하지만 호텔 관련 어플리케이션을 만들면서 그 중요성에 대해 알게 되었다. 어플리케이션의 주요 기능은 호텔 고객의 영수증 인쇄를 도와주는 것이었다. 1년 후, 호텔 경영진은 어플리케이션에 몇 가지 새로운 기능 구현을 요청했다. 불행히도, 기존 어플리케이션의 구조가 너무 형편없었기 때문에 어플리케이션 전체를 다시 작성해야 했다. 작은 기능 하나가 어플리케이션 전체에 큰 폭풍을 불러 일으켰다. 더 많은 기능 구현을 하며 코드를 재작성하는 일이 두 번 더 반복되었고, 결국 나는 프로젝트 지원을 포기했다. 그리고 결국 다른 사람을 고용하여 어플리케이션을 재작성하였다. 지금 생각해보면, 이전에 작성한 경멸스런 코드와 고객에게 나쁜 경험을 준 것이 후회된다.
필자는 Brad Frost가 소개한 아토믹 디자인을 통해 많은 부분을 개선하였다. 어플리케이션 전체적인 관점에서 문제를 파악하고 해결하는 시야가 생겼고 이로 인해 코딩 습관도 완전히 바뀌었다.
이 글의 목적은 프런트엔트 어플리케이션을 구조화하는 방법을 이해하도록 돕는 것이다. 아토믹 디자인은 사용자 인터페이스를 작고 단순한 요소로 분리하는 개념이며, 일관된 UI를 만들고 유지보수성을 향상시켜준다.
리액트의 가장 흥미로운 개념 중 하나가 컴포넌트 기반의 프로그래밍이라고 생각한다. 하지만 많은 개발자들은 이 개념을 충분히 활용하지 못하고 DRY 원칙을 위반한다. 이런 현상은 리액트 공식 문서에 설계 패턴의 표준 모범사례가 없고, 대부분의 코드가 각 개발자의 개인 의견에 따라 작성되기 때문일 수 있다. 하지만 리액트 공식 문서에는 컴포넌트 구성에 대한 훌륭한 예제들이 많으므로, 이를 따르는 것을 추천한다.
이런 개념들은 대부분의 프런트엔드 프레임워크에 적용될 수 있지만, 필자는 가장 익숙하고 편리한 리액트를 선택했다(페이스북은 리액트를 UI 라이브러리라고 부르지만 여기서 다룰 이야기는 아니다).
또한 리액트는 프런트엔드 개발을 위해 설계 우선 접근법을 권장한다. UI/UX 팀은 개발자들이 구현할 수 있는 목업을 만들 것이며(mocking), 개발자들은 이를 이용해 UI를 컴포넌트 계층으로 세분화 할 수 있다. 계층 구조를 기반으로 컴포넌트들을 작성하는 것은 아토믹 디자인에서 중요한 기초 작업이다. 원자, 분자, 유기체, 템플릿, 페이지의 개념은 이 계층 구조를 형성하는 데 도움이 된다.
필자는 규모가 큰 리액트 단일 페이지 어플리케이션(SPA)에 이 방법론을 사용하기 시작하였다. 작성해야할 컴포넌트들이 많이 있었지만, 그 당시의 CSS 프레임워크나 라이브러리와는 맞지 않는 부분이 있었다. 그래서 HTML 요소과 커스텀 CSS를 사용하여 컴포넌트를 작성하기로 결정하였다. 컴포넌트에서 모든 것이 관리된다는 개념이 흥미로웠고, 실제로 개발 생산성도 높아졌다.
Brad Frost는 화학에서 용어를 빌려 아토믹 디자인 개념을 UI 구성요소와 함께 활용하는 방법을 소개하였다. 웹 사이트의 구성 블록이 HTML 요소들이며, 이 요소들은 원자라고 볼 수 있다. 원자가 결합되어 분자가 되는 것처럼 HTML 요소들은 결합되어 복잡한 페이지를 형성한다. 마찬가지로 각 페이지는 컴포넌트(또는 HTML 요소)로 분해될 수 있으며, 분해된 컴포넌트는 화학에서 가르치는 분자, 유기체와 유사하다.
아토믹 디자인은 원자(Atoms), 분자(Molecules), 유기체(Organisms), 템플릿(Templates), 페이지(Pages)로 효과적인 인터페이스 시스템을 만든다.
원자는 버튼, 제목, 텍스트 입력 필드와 같은 가장 작은 구성 컴포넌트이다. 원자는 모든 컴포넌트들의 기초가 되는 블록이며, 더 이상 분해 될 수 없는 필수 요소이다. 분자는 2개 이상의 원자로 구성되어 있으며, 하나의 단위로 함께 동작하는 UI 컴포넌트들의 단순한 그룹이다. 예를 들어 HTML 텍스트 입력 필드, 레이블, 오류 메세지 또는 HTML 텍스트 입력 필드와 버튼으로 구성된 검색 컴포넌트가 있다. 유기체는 분자, 원자 또는 다른 유기체의 그룹으로 구성된 비교적 복잡한 그룹이다. 이 유기체들은 인터페이스의 개별적인 영역을 형성한다.
템플릿은 컴포넌트들을 배치하고 설계의 구조를 보여준다. 페이지의 실제 컴포넌트가 없을 경우, 페이지가 어떻게 보일지에 대한 골격 구조이다. 페이지는 실제 컨텐츠들을 배치한 UI를 보여주며, 템플릿의 구체화된 인스턴스이다. 템플릿과 페이지는 유기체, 분자, 원자를 포함하고 있다. 이러한 작은 구성 요소들의 결합은 어플리케이션에서 사용자 인터페이스를 구성한다.
앞에서 설명한 것들을 사용하여 코드 베이스에 대한 폴더 구조를 구체화 할 수 있다. 여기서 중요한 것은, 폴더 구조는 코드 베이스에 따라 달라질 수 있으니 구현 전에 반드시 팀원들과 상의해야 한다는 것이다. 위의 이미지를 보면, 프런트엔드에서 사용되는 모든 컴포넌트를 저장할 components 폴더가 있고, 페이지 레벨에서 분리할 수 있는 작은 컴포넌트를 저장할 UI 폴더가 있다.
팀원들에게 반복적으로 자주 들었던 질문들 중 하나가 비즈니스 로직에 의존하는 내부 상태를 사용하는 컴포넌트들은 어떻게 해야 하는가였다. 이런 경우 내부 상태를 어플리케이션의 비즈니스 로직에서 분리하는 것이 중요하다(드롭다운 버튼의 열림, 닫힘이 내부 상태를 가진 컴포넌트의 좋은 예이다). 컴포넌트들은 오직 props의 업데이트로 인해 업데이트되는 더미 컴포넌트와 같다.
어플리케이션과 분리하여 컴포넌트를 개발하고 테스트할 수 있으며, 스타일 가이드와 같은 도구에서 볼 수 있다. 그리고 통합 개발을 할 때, 백엔드 어플리케이션의 로직에 의존하지 않는다는 장점이 있다.
일련의 패턴이 확립되면, 설계 변경이 필요한 경우에 대비하여 더 빠르고 유연성 있는 빌드 프로세스를 가질 수 있다. 또한 기존의 컴포넌트들을 재사용하고 있기 때문에 디자인을 일관성 있게 통일할 수 있다.
특정 컴포넌트에 CSS가 강하게 결합되어 있기 때문에 CSS를 훨씬 잘 관리할 수 있다. 이를 위해서는 어플리케이션의 구조에 따라 컴포넌트에서 사용되는 CSS만 렌더링하도록 해야 한다.
컴포넌트가 분리되어 있고 상위 컨테이너 컴포넌트의 사이즈를 결정할 수 없을 경우, 미디어쿼리를 사용하기 어렵다. 컴포넌트는 너비에 대해 알 수 없기 때문에 실제 페이지의 사이즈가 변경될 때 크기가 조정된다.
이 문제는 크기를 조정할 수 있는 flex, grid 와 같은 CSS 속성을 구현한 레이아웃 컴포넌트를 도입하여 해결 할 수 있다.
위의 폴더 구조 이미지에서 어플리케이션은 단지 컴포넌트들로만 구성되어 있다. 컴포넌트들을 업데이트하기 위해 필요한 것은 비즈니스 로직을 가져오는 것이다. 필자는 뷰와 비즈니스 로직을 분리하는 것(관심사의 분리)이 좋은 방법이라고 생각한다. 이렇게 하면 프로젝트가 확장될 때 코드에서 문제를 디버깅하기 더 쉽다. 우리는 다른 접근법을 적용하여 관심사의 분리를 적용할 수 있었다. 개인적으로 고차 컴포넌트(HOC) 사용을 선호하기때문에 이를 이용하여 props로 특정 페이지의 상태를 변경하였다. 이 상태는 어플리케이션에서 공유되는 일련의 API 호출일 수도 있다. 다른 해결 방법으로는 render props 또는 리액트 hook이 있다.
좀 더 추상적으로 설명하자면, 어플리케이션의 컴포넌트들은 아래 이미지처럼 동일한 색상과 크기를 가진 빈 병이라고 할 수 있다. 각각의 병을 다른 색으로 채운다고 생각해보자. 이 과정은 컴포넌트의 상태를 업데이트하는 것으로 비유할 수 있다. 빈 병에 색을 채우는 것은 쉬운 일이고, 색을 바꾸는 상태 변경이 있더라도 빈 병에 넣을 색을 변경하는 것이기 때문에 쉬운 과정이 될 것이다.
하지만 만약 병을 빨간 색으로 반쯤 채우고 다른 색으로 나머지를 채운다고 한다면 문제가 있을 것이다. 그 색이 잘 섞이지 않는다면? 또는 원래의 색을 제거하고 다른색으로 대체해야한다면? 이것은 비즈니스 로직을 구현할 때도 발생할 수 있는 문제로 코드 리팩터링과 컴포넌트 재사용을 어렵게 만든다.
한가지 더 말할 것이 있다.
리액트 공식 문서에 나온 것처럼, 단일 프로젝트에서 폴더의 중첩은 최대 3개 또는 4개로 제한할 것을 권장한다. 상대경로 import는 jest, webpack 또는 babel 에서 제공하는 alias 를 사용하여 깔끔하게 정리할 수도 있다.
리덕스를 사랑하는 사람들은😛 Katia Wheeler의 좋은 글을 참고하면 된다.(참고문헌에 있다).
또한 프로젝트에서 이것을 어떻게 적용해야 하는지에 대한 좋은 예시를 제공해준 나의 전 동료이자 프런트엔드 마에스트라 Ugonna Thelma에게 감사하고 싶다. 이 글의 작성에 도움을 준 Chidiebere Japheth Anyigor, Shehu Muhammad와 Segun Ola에게 특별히 감사한다.