폴더구조를 어떻게 가져가나는 팀 마다 정하기 나름이기 때문에
항상 고민이 되는 주제인 것 같다.
도메인을 기준으로 나눌 것인지, 페이지 별로 나눌 것인지부터 시작하여 hook들을 하나로 묶을 것인지 등등 여러가지 규칙이 존재한다.
공식또한 어떤 기준으로 폴더를 나누고 파일들을 정리할 것인지 고민을 많이 하였다. 어떻게 나누어야 우리 뿐만 아니라 다른 개발자가 들어와서 보았을 때에도 파일들을 찾기 쉬워질까 말이다.
공식의 폴더 구조의 경우 초반과 현재 조금 달라지게 되었다.
해당 이유와 역사들을 짚어보고 가보고자 한다.
기존의 공식 : 페이지별로 나누자 📖
최상위 디렉토리
최상위의 경우, src 와 public을 제외하고는 프로젝트의 설정이 들어가는 곳으로 작성된 코드들이 위치할 src와 레벨을 동일하게 하였다. public은, webpack의 빌드 결과가 저장되는 폴더이다.
src 의 구조
- api
비동기 통신의 로직들이 모여있는 곳이다
- assets
서비스에서 사용되는 이미지를 보관하는 장소이다
- components
pages들 간에 공통적으로 사용되는 컴포넌트를 모아두는 곳이다.
대표적으로는 Button 컴포넌트 등이 있다.
- constants
상수들을 관리하는 곳이다.
- hooks
pages들 간에 공통적으로 사용되는 hook들을 모아두는 곳이다.
대표적으로는 useSnackBar가 있다.
- mock
서버 모킹을 위한 msw와 관련된 로직들을 모아둔 곳이다.
- pages
각각 페이지 별로 컴포넌트와 훅들을 묶어놓은 곳이다.
- store
recoil을 활용한 전역 상태들을 저장하는 곳이다.
- styles
공통 style인 theme 과 reset 스타일등을 모아둔 곳이다.
- types
공통 type들을 모아 둔 곳
- utils
공통적으로 사용되는 함수, 분리할 필요가 있는 함수들을 모아두는 곳이다.
예를 들어 사용자의 입력값의 유효성을 평가하는 validate로직과 여러 컴포넌트 간에 사용되는 서버에서 오는 정보를 변환하여 보여주는 converter로직을 모아둔 곳이다.
- index.tsx 와 App.tsx
index.tsx의 경우 리액트의 설정들이 위치해있고
App.tsx에서는 Router를 통해서 페이지간 연결을 해주었다.
pages의 구조
- pages 안에는 현재 공식에 존재하는 페이지 별로 폴더를 나누었다.
- index.tsx, index.styles.tsx, index.stories.tsx
각각 페이지의 구현, 스타일링, 스토리북이다.
- PopularArticles
페이지 내에서만 사용되는 페이지에서 분리된 컴포넌트이다.
해당 폴더에는 PopularArticles.tsx, PopularArticles.styles.tsx, PopularArticles.stories.tsx 가 존재한다.
- hooks
해당 페이지 내에서만 사용되는 훅으로 이루어져 있다.
그리고 훅에 대한 태스트 코드가 해당 폴더의 하위에 위치한다.
왜 page 별로 나누었을까? 🤔
"응집성"을 기준과 초기의 개발 방식에 의해서 page별로 나누게 되었다.
우선, 관련 있는 로직은 가까이에 위치해 둔다는 응집성의 원칙에 따라 페이지에 종속적인 (=해당 페이지에서만 사용되는) 컴포넌트와 훅들을 가까이에 모아두었다.
다음으로는, 초기에는 페이지 별로 기능을 구현해 나갔기 때문에 한 페이지의 폴더 내에서 사용되는 hooks들과 component, storybook 등을 선언하여 빠른 개발이 가능하였다.
어떤 불편함을 느꼈을까? 🤔
depth가 깊다!
우아한테크코스에서 필수 요구사항으로 테스트 코드를 짜는 것이 필요했다. 그러기 위해서 이미 구현된 hook들에 대해서 일일이 접근하는 과정이 필요했었는데
여기서 문제가 발생하였다!
"생각보다 depth가 깊었다"
만들 때에는 폴더 별로 만들 다 보니 폴더간 이동을 할 필요가 없었는데, 테스트 코드 작성을 위해서 hook을 접근하려니, 각각의 폴더를 열어서 다시 hook으로 들어가 test 폴더를 만들어야 했다. 추가로 test 코드들을 관리하는 곳이 pages > 사용되는 page폴더 > hooks > test 이렇게 4단계를 내려갔어야 했어서 관리가 힘들었다.
게다가, 과거에 비해서 페이지수가 많아지다 보니 어디에 어느 hook이 존재했는지도 잘 기억이 나지 않았었다. 그래서 결국은 하나 하나 페이지 폴더를 열어 찾아가야 하는데, 생각보다 시간이 오래걸리게 되었다.
또한 스토리북을 대대적으로 수정해야할 일이 있었는데 해당 경우 또한 각각의 페이지 별로 접근하여 수정을 해주어야 해서 파일을 찾아가는데 더 많은 시간이 걸렸다.
페이지 수가 이것 보다 더 많아지면, 더 많은 손가락 노동이 필요해보였다. 😇
폴더 위치를 변동해야하는 경우
공식 서비스가 커지면서 기존의 페이지에 종속된 로직을 다른 페이지 컴포넌트에서 사용해야하는 경우가 있었다.
예를 들어 ToastEditor의 경우가 있었다.
ToastEditor는 마크업 문서를 작성하는 것을 도와주는 외부라이브러리로 초창기에는 글을 작성하는 페이지에서만 사용되었으므로 해당 폴더에 종속되어있었다.
하지만, 댓글에서도 마크업 기능을 도입하게 되면서 ToastEditor 컴포넌트가 공통 컴포넌트로 이동되었어야 했다. 그래서 pages의 폴더에서 common으로 이동이 되었었다.
해당 케이스에 대해서 더 자세히 관찰해보자.
ToastEditor의 경우 어떤 페이지에서 종속적으로 사용되고 있는지 개발자가 알고 있었기에, (=과거에 개발을 했기에) 바로 해당 페이지 아래에서 컴포넌트를 찾아내어 common으로 이동시킬 수 있었다.
하지만, 페이지의 수가 더 많아지고 여러 개발자가 같이 개발을 해나가다 보면 개발자 개개인이 어떤 페이지에 어떤 컴포넌트나 훅이 존재하는지 파악하기 힘들다.(특히, 팀 함류전에 만들어진 페이지라면 더 그럴 것이다) 만일 파악한다 하더라도 페이지 별로 들어가봐야 한다는 단점이 존재하기에 탐색에 시간이 걸릴 수 있고 그것을 계속 기억하고 있어야 하는 단점이 존재한다. 그래서 다시 재사용 가능한 컴포넌트가 페이지 단에서 존재하지만 이를 모르고 똑같은 컴포넌트를 새롭게 페이지에 종속적으로 만들 수 있다.
즉, 페이지 수가 많아짐에 따라서 발생하는 시간이 늘어난다.
관심사끼리 묶어서 관리하자 🖇
- 기존과 동일한 src 밑의 파일 구조에서 storybook과 test 폴더가 새롭게 생성되었다!
storybook과 test를 묶자
- storybook을 하나씩 접근하여, 수정해야하는 경우 들에 각각의 storybook의 위치를 파악하기 힘들다는 점을 겪었기에 향후 관리를 위해서 storybook 하위에 모두 storybook 파일들을 위치시켰다.
또한 해당 스토리북을 도메인 별로 분리하여, 찾기 쉽도록 하였다.
- test 코드들도 동일하게 하나의 폴더로 모으고, 도메인 별로 분리하였다.
components의 개편 💪
- 페이지에 종속되어 pages하위에 위치했던 컴포넌트들을 모두 componentes 폴더로 이동시키고, 해당 폴더들을 storybook에서와 동일하게 도메인별로 분리하였다.
기존에 components 하위에 있던 공통적으로 사용되는 파일들의 경우 common으로 묶었다. 추가로 다른 페이지에 종속적인 컴포넌트 폴더와 구분하기 위해 @을 앞에 폴더명 앞에 붙여 맨 앞에 위치하도록 하였다.
hooks도 동일하게
- hook도 동일하게, pages폴더 밑에서 나와 하나의 hooks 하위에 위치하도록 하였다.
새롭게 단장된 pages
- 이렇게 기존에 pages 하위의 컴포넌트와 훅들을 밖으로 빼내옴으로써, 페이지 폴더 밑에는 페이지 자체를 구현한 컴포넌트와 스타일만 남도록 하였다.
새롭게 추가된 api 폴더 하위의 규칙 (그래도 응집성은 포기할 수 없어!)
- api 역시도 hook, storybook 등과 같이 도메인 별로 묶게 되었다.
현재는 아직 pr로 올라와 있어 main이나 develop에는 반영이 되지 않았지만, 기존에는 types라는 폴더 밑에서 관리되었던 비동기 통신을 하는데 필요한 서버에서 요청할 때 필요한 요청 타입, 또 서버로부터 돌아올때의 응답타입을 하나의 articleType이라는 파일에서 관리하게 되었다.
api > 도메인 폴더 로 2단계만 거치면 접근이가능하기 때문에 해당 경우에는 응집성을 지키기 위해 api 폴더 하위에서 사용되는 도메인 폴더 밑에 두기로 결정하였다.