React 프로젝트의 디렉토리 구조

리액트와 Flux를 도구로 SPA를 만든 지 약 1년 반, 그 동안의 경험과 이를 토대로 구상하고 사용중인 디렉토리 구조를 소개합니다.

종류별 분류

최초로 사용했던 구조는 아래와 같았습니다.

/app

주요 코드는 액션, 컴포넌트, 페이지, 스토어. 네 폴더에 담겨있었죠. constants나 dispatcher에는 앱 전체에서 공유하여 사용하는 하나의 파일만 존재하는 구조였습니다.

문제점

이 구조에서 불편했던 점은, 하나의 페이지를 수정하기 위해서는 해당 페이지에 연결된 액션, 스토어, 상수, 그리고 컴포넌트가 각각의 폴더에 분리되어있다는 점이었습니다. 이 폴더를 열었다가 저 폴더를 열었다가…

이 문제를 해결해보고자, 두 번째 프로젝트에서는 전혀 다른 관점의 디렉토리 구조를 가져가보게 되었습니다.

페이지별 분류

/app/application

한 페이지를 개발할 때에는, 하나의 폴더 내에서 대부분의 작업을 할 수 있도록, 페이지 별로 구조를 나누었습니다. URL과 거의 같은 구조로 폴더를 찾아갈 수 있어 편리합니다. assets는 소스와 분리하기 위해 한 단계 위에서 분리됩니다.

/app/application/contents

Contents 디렉토리 안에서는 콘텐츠 등록이나 수정 등의 콘텐츠와 관련된 페이지를 모아둡니다. 비슷한 대상을 다루는 페이지에서는 공통의 스토어와 액션을 사용하기 위해 모아두었습니다.

문제점

하지만 점차 페이지별로 기능과 구성이 더해지면서 페이지 별로 스토어나 액션을 따로 구성하는 것이 편하게 코딩할 수 있다는 것을 느꼈습니다. 세 개의 페이지를 모아두면, 세 개의 스토어와 세 개의 액션을 만들고 있었습니다. 대단위 url path로 모아두는 의미가 줄어들고 있었습니다.

또한, 앱 전체에서 공통적으로 사용하게 되는 컴포넌트를 모아두기 위해 common이라는 디렉토리가 생기고, 점차 common 디렉토리가 비대해졌습니다. 이것만으로는 별 문제가 된다고 말 할 수는 없지만, 자신의 store를 가져야 하는 컴포넌트와 HigherOrderComponents가 common에 포함되기 시작하면서 구조가 더욱 복잡해졌습니다.

Atomic Design

지금은 다음과 같은 구조로 바꾸어가고 있습니다.

/app/src

pages > system(Components) > (basic)components 로 이어지는 3단 구조입니다. Atomic design의 개념을 많이 가져오려고 노력했습니다.

atomic design

여기에서 <span>, <button> 같은 기본 html element를 atom이라고 생각합니다.

molecules에 해당하는 대상은 그런 atom을 적당히 모아 사용하기 쉽게 구조화했거나, 기본 html 태그를 래핑한 리액트 컴포넌트들입니다. 이런 컴포넌트들을 components 폴더에 모아두었습니다. 재사용될 일이 많기 때문에 최대한 시스템과 독립적으로 행동할 수 있도록, 액션이나 스토어와 관련없이 독립적으로 동작해야 한다는 원칙을 세웠습니다. 다른 프로젝트에 그대로 가져다 써도 문제가 없을 정도의 독립성을 유지했습니다. 이를 따르기 위해서 이 레벨에 존재하는 대부분의 컴포넌트의 propTypes에는 onChange 또는 onClick 함수가 포함됩니다.

/app/src/components

대부분 기본 엘리먼트로 대체할 수도 있지만, 추가적인 기능을 더하거나 디자인적 추가 요소의 필요성 때문에 리액트 컴포넌트로 래핑된 녀석들입니다.

/app/src/system/post

그리고 그런 녀석들과는 다르게, 시스템(DDD의 도메인과 비슷한 개념이라고 볼 수 있습니다)과 연관되어 도메인의 개념을 나타내는 컴포넌트 — 서버에서 받아온 json 응답의 형식에 의존적 — 이거나, 해당 컴포넌트에 액션이 포함된다거나 — ajax 요청을 통해서만 초기화 될 수 있다거나, ajax 요청을 보내야 하거나, 팝업을 띄우는 등 — 하는 컴포넌트 들은 system으로 묶입니다. 이 녀석들이 atomic design 의 관점에서 보자면 organism 에 해당되는 컴포넌트들입니다. 물론, 이 레벨에서 작성된 대부분의 컴포넌트가 molecules 레벨의 컴포넌트를 사용하고 있습니다. 반대 방향의 의존성은 없습니다.

그리고 organisms을 모은 templates 대신, 페이지 조각 라고 해야 할까요. 페이지와, 이런 페이지를 영역별로 조각조각 나눈 part-of-page 컴포넌트들이 pages 에 페이지를 구성하는 컴포넌트와 함께 들어있습니다.

/app/src/pages/PostList
/app/src/pages/PostList/PostList.js의 render(). 페이지 조각을 이렇게 사용한다.

마지막 단계에 존재하는 Page는 직접 Store/Actions 와 데이터를 주고받아, 페이지 조각 컴포넌트에게 전달받은 데이터를 내려주는 형태를 띄고 있습니다. 각 페이지마다 액션과 스토어를 따로 관리하고 있죠.

캡쳐된 파일 이름을 자세히 살펴보신 분이라면 (거의 없겠지만) system/post/PostActions 와 pages/PostList/PostListActions 에 어떤 차이점이 있는지 궁금하실 수도 있겠습니다. 앞의 것은 단일 포스트에 대한 것으로, 포스트라는 개념을 표현하는 컴포넌트에서 사용되는 액션들을 모아놓은 것입니다. 포스트라는 개념이 단순히 포스트리스트라는 하나의 페이지에서만 표현되는 것이 아니라, 다른 페이지에도 반복적으로 표현되고 액션을 수행할 수 있도록 열려있기 때문에 위와 같이 분리되었습니다. 한편, 포스트 목록을 받아오는 액션은 PostList 페이지에서밖에 사용되지 않습니다. 따라서 PostList 페이지 내에 위치시켰습니다. 추후 포스트리스트 역시 반복적으로 사용되도록 기획상의 변경이 일어난다면, 이 액션 역시 system으로 옮겨야 하겠지요.

현재까지는 충분히 깔끔하고 명확한 구조라고 느끼고 있습니다. 다만 이런 새로운 구조를 적용하는 과정에서 기존 구조의 모든 것을 한 번에 바꾸기에는 일이 너무 커, 중간 구조(mixed state)를 거쳐 리팩토링을 진행하고 있습니다. 다 바꾸고 나면 매우 후련할 것 같네요.

Show your support

Clapping shows how much you appreciated HyeonSeok Yang’s story.