2025. 4. 2. 18:23ㆍStudy/React, next.js
태초마을로 오기 전까지의 우리 팀 프로젝트 구조는
아마 AI도 이해를 포기할법한 아키텍쳐였을 것이다.
2023년 11월, 약 4년만에 프론트엔드 바닥에 다시 들어왔다.
나의 수습 기간이 끝나기도 전에 파트장님과 팀원 1명 퇴사해버렸다.
그렇게 우리 파트에는 나를 포함하여 3년차 팀원 1명, 총 2명이었다.
총 연차로만 따지면 내가 제일 높아서 직급도 내가 위였다.
최근 4년동안은 개발자라기 보단 직장인에 가까웠다.
개발 공부보다 재테크 공부가 재밌기도 했고(크흠), 안정형을 추구했다.
적당히 잘 나오는 매출, 문제없이 잘 돌아가고 있는 서비스
게임서버 담당 나 혼자인데 최신 기술 사용해보다 뻑나면...? 버전 업 하다 뻑나면...? 나혼자 감당가능...?😭
기술적인 부분에 갈증을 느끼게 되면서, 어쩌면 화초처럼 자라고 있을지도 모른다는 생각이 들었다.
그래서 나와 쿵짝이 잘 맞는 동료들, 딱히 불만없던 회사를 뒤로하고 지금 팀에 합류하였다.
내가 원하는 곳을 확실하게 긁어주었다.
최신 기술과 유행하는 기술을 민감하게 반응하여 빠르게 적용해보고 싶어하는 동료의 모습에
'역시 프론트엔드 개발자! 빠르게 변화하는 생태계에 적응해야되는군!' 하고 자극받으며, 직급 생각 말고 많이 가르쳐달라 하였다.
그래서 요즘엔 이런거 쓴대요~ 이런게 나왔어요~ 하시면서 종종 적용해보자고 하시길래
나보다 더 잘 아시겠지 ^0^
생각했던 것보다 훨씬 생태계 변화가 빠르구나! ^0^
되게 똑똑하시다 ^0^
라고만 생각했다.
그렇게 함께 구조 리팩토링의 늪에 빠지게 되었다.
참고로 Next.js 14 / React 18 기준
1. 어설픈 Atomic의 시작
├── .config
├── .husky
├── .storybook
├── api
│ ├── // api fetch 관련
├── app
│ ├── (homeLayout)
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ ├── search
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ ├── global-error.tsx
│ ├── layout.tsx
│ ├── not-found.module.scss
│ ├── not-found.tsx
├── components
│ ├── atoms
│ │ ├── Button
│ │ │ ├── index.tsx
│ │ │ ├── Button.module.scss
│ ├── molecules
│ ├── organisms
│ │ ├── TabMenu
│ │ │ ├── index.tsx
│ │ │ ├── TabMenu.module.scss
│ ├── pages
│ │ ├── MainHome
│ │ │ ├── _fragments
│ │ │ │ ├── MainBanner
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── MainBanner.module.scss
│ │ │ │ ├── Notice
│ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── Notice.module.scss
│ │ │ ├── index.tsx
│ │ │ ├── MainHome.module.scss
│ │ ├── Search
│ │ │ ├── index.tsx
│ │ │ ├── Search.module.scss
│ ├── templates
│ │ ├── Header
│ │ │ ├── index.tsx
│ │ │ ├── Header.module.scss
│ │ ├── Footer
│ │ ├── Sidebar
├── deploy
│ ├── // cicd 관련
├── mock
│ ├── // mockoon 관련
├── public
│ ├── assets
│ │ ├── fonts
│ │ ├── svgs
│ │ ├── images
│ ├── favicon.ico
├── recoil
│ ├── // recoil 관련
├── shared
│ ├── constant
│ ├── hooks
│ ├── types
│ ├── utils
├── stories
│ ├── // storybook 관련
├── styles
│ ├── global.scss
│ ├── tinyReset.scss
│ ├── font.scss
완전 초기 구조와 달라진 점을 간략하게 설명하자면
/components 안에 도메인(페이지)별로 나눠져 있던 디렉토리를 atoms, molecules, organisms, templates, pages로 쪼개두었다.
🚨문제 발생🚨
아토믹 디자인을 사용할수록 특히 molecules, organisms 구분이 모호해졌다.
그리고 지금와서 보니 templates에 들어있는 header, footer, sidebar가 organisms에 들어가야했던건 아닐까 싶기도...
아무튼 사람마다 생각하는게 다르고 기준이 다르다보니, 어느 쪽에 어느 컴포넌트를 넣느냐에 대해 시간을 소요하게 되었고
코드 리뷰 시에도 그 부분이 논쟁 거리가 되어 오히려 생산성을 떨어뜨리고 고민만 늘어나는 결과를 얻게 되었다.
그렇게 불편함을 겪고 있던 어느 날
"FSD 요즘 많이 적용하는 것 같던데 FSD를 아는 사람이라면 오자마자 적응 100% 가능할걸요?"
라는 말에 설득 당해, 점진적으로 FSD 도입해보자고 결론을 짓게 되는데...
2. Atomic이 믹스된 대환장 FSD
├── .config
├── .husky
├── .storybook
├── app
│ ├── (homeLayout)
│ │ ├── layout.tsx
│ │ ├── page.tsx // src/pages/MainHome/index.tsx 가져와 사용
│ ├── (subHome)
│ │ ├── layout.tsx
│ │ ├── page.tsx // src/pages/SubHome/index.tsx 가져와 사용
│ ├── search
│ │ ├── layout.tsx
│ │ ├── page.tsx // src/pages/Search/index.tsx 가져와 사용
│ │ ├── result
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx // src/pages/Search/index.tsx 가져와 사용
│ ├── global-error.tsx
│ ├── layout.tsx
│ ├── not-found.module.scss
│ ├── not-found.tsx
├── deploy
├── mock
├── public
├── src
│ ├── app
│ │ ├── head
│ │ ├── provider
│ │ ├── script
│ │ │ ├── GA
│ │ ├── styles
│ │ │ ├── mixins.scss
│ │ │ ├── font.scss
│ │ │ ├── global.scss
│ │ │ ├── tinyReset.scss
│ ├── entities
│ │ ├── MainBanner
│ │ │ ├── apis
│ │ │ │ ├── fetchBanner.ts
│ │ │ ├── models
│ │ │ │ ├── index.ts
│ │ ├── QuickLink
│ │ │ ├── apis
│ │ │ │ ├── fetchQuickLink.ts
│ │ │ ├── models
│ │ │ │ ├── index.ts
│ │ ├── user
│ │ │ ├── apis
│ │ │ │ ├── fetchUser.ts
│ │ │ ├── models
│ │ │ │ ├── index.ts
│ │ │ ├── query
│ │ │ │ ├── index.tsx
│ │ │ │ ├── queryOptions.ts
│ ├── features
│ │ ├── MainBanner
│ │ │ ├── ui
│ │ │ │ ├── MainBanner.module.scss
│ │ │ │ ├── index.tsx
│ │ ├── QuickLink
│ │ │ ├── constant
│ │ │ │ ├── index.ts
│ │ │ ├── ui
│ │ │ │ ├── MainBanner.module.scss
│ │ │ │ ├── index.tsx
│ ├── page
│ │ ├── MainHome
│ │ │ ├── index.tsx
│ │ │ ├── ui
│ │ │ │ ├── MainBanner
│ │ │ │ │ ├── index.tsx // features/MainBanner 가져와 사용
│ │ │ │ ├── QuickLink
│ │ │ │ │ ├── index.tsx // features/QuickLink 가져와 사용
│ │ ├── Search
│ │ │ ├── apis
│ │ │ │ ├── fetchSearch.ts
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ ├── libs
│ │ │ │ ├── index.ts
│ │ │ ├── models
│ │ │ │ ├── index.ts
│ │ │ ├── query
│ │ │ │ ├── index.tsx
│ │ │ │ ├── queryOptions.ts
│ │ │ ├── ui
│ │ │ │ ├── SearchHeader
│ │ │ │ │ ├── SearchHeader.module.scss
│ │ │ │ │ ├── index.tsx
│ │ │ │ ├── SearchHome
│ │ │ │ │ ├── SearchHome.module.scss
│ │ │ │ │ ├── index.tsx
│ │ │ │ ├── SearchResult
│ │ │ │ │ ├── ui
│ │ │ │ │ │ ├── SearchAll
│ │ │ │ │ │ │ ├── SearchAll.module.scss
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── SearchKeyword
│ │ │ │ │ │ │ ├── SearchKeyword.module.scss
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ ├── SearchNews
│ │ │ │ │ │ │ ├── SearchNews.module.scss
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── SearchResult.module.scss
│ │ │ │ │ ├── index.tsx
│ │ ├── SubHome
│ │ │ ├── index.tsx
│ │ │ ├── ui
│ │ │ │ ├── MainBanner
│ │ │ │ │ ├── index.tsx // features/MainBanner 가져와 사용
│ │ │ │ ├── QuickLink
│ │ │ │ │ ├── index.tsx // features/QuickLink 가져와 사용
│ ├── shared
│ │ ├── API
│ │ │ ├── constant
│ │ │ ├── lib
│ │ │ ├── model
│ │ ├── UIKit // atomic 디자인의 잔재..
│ │ │ ├── atoms
│ │ │ ├── molecules
│ │ │ ├── organisms
│ │ ├── constant
│ │ │ ├── index.ts
│ │ ├── lib
│ │ │ ├── hooks
│ │ │ │ ├── index.ts
│ │ │ ├── utils
│ │ │ │ ├── index.ts
│ │ ├── model
│ │ │ ├── store
│ │ │ │ ├── atoms.ts
│ │ │ │ ├── selectors.ts
│ │ │ ├── types
│ │ │ │ ├── index.ts
│ ├── widgets
│ │ ├── Footer
│ │ ├── Header
│ │ ├── Sidebar
├── stories
(위 보다 훠어얼씬 복잡하고.. 얘는 왜 여기 들어가있지? 하는게 많았다)
제일 오래 유지한 구조였다.
인력난(5명이 TO인 팀에 2명남음..)으로 인해 리팩토링할 시간이 나지 않아 억지로 유지했을지도 모른다. (네 다음 변명🤦♀️)
사실 FSD 도입 시작부터 삐걱거리긴 했다.
❌ 삐그덕 1. 예약어의 충돌
Next.js의 app router를 사용하다보니 root단부터 FSD 레이어를 적용하면 오류가 발생하였다
✅ 해결방법 : 그래서 src 디렉토리를 만들어 그 아래로 FSD 레이어를 두었다.
├── app # NextJS app folder
├── src
│ ├── app # FSD app folder
│ ├── entities
│ ├── features
│ ├── pages
│ ├── shared
│ ├── widgets
❌ 삐그덕 2. App Router/Page Router 혼동
App router인데 Page router(src > pages가 있어서)로 헷갈려하는 것 같다.
✅ 해결방법 : pages에 s를 빼고 src > page 로 사용
├── app # NextJS app folder
├── src
│ ├── app # FSD app folder
│ ├── entities
│ ├── features
│ ├── page # pages -> page
│ ├── shared
│ ├── widgets
🚨문제 발생🚨
이해하기 어려운 구조와 깊어지는 depth
어찌저찌 위 삐그덕 난관들을 헤치고 FSD를 도입하여 초반에는 잘 유지가 되었다고 생각한다.
그리고 남아있는 Atomic 구조는 점진적으로 리팩토링하기로 동료와 합의하였다.
하지만 코드 리뷰도 못할 정도로 정신 없는 시기를 6개월 정도 보내고, 그 사이에 동료는 퇴사엔딩...
의도를 물어볼 사람 없고, 나 조차도 점점 FSD를 제대로 이해하지 못한 채 사용하고 있다는 생각이 들었다.
1. 프로젝트가 거대해질수록 오히려 depth가 너무 깊어졌고,
2. entities, features, shared로 뿔뿔히 흩어지다보니 너무 분리되어있다고 느껴졌고,
3. 나 빼고 팀이 물갈이가 되었는데 새로 합류하신 동료들이 입을 모아 하시는 말씀.. "이해하기 어렵다."
3. 결국 태초마을st로
├── .config
├── .husky
├── .storybook
├── app
│ ├── (homeLayout)
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ ├── search
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ ├── (subHome)
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ ├── global-error.tsx
│ ├── layout.tsx
│ ├── not-found.module.scss
│ ├── not-found.tsx
│ ├── styles.scss // src/common에 있는 style import용
├── deploy
├── mock
├── public
├── src
│ ├── common
│ │ ├── apis
│ │ ├── components
│ │ │ ├── mainBanner
│ │ │ │ ├── MainBanner.module.scss
│ │ │ │ ├── index.tsx
│ │ │ │ ├── apis
│ │ │ │ │ ├── fetchMainBanner.ts
│ │ │ │ ├── constants
│ │ │ │ │ ├── index.ts
│ │ │ │ ├── types
│ │ │ │ │ ├── index.ts
│ │ │ ├── quickLink
│ │ │ │ ├── QuickLink.module.scss
│ │ │ │ ├── index.tsx
│ │ │ │ ├── apis
│ │ │ │ │ ├── fetchQuickLink.ts
│ │ │ │ ├── constants
│ │ │ │ │ ├── index.ts
│ │ │ │ ├── types
│ │ │ │ │ ├── index.ts
│ │ ├── constant
│ │ ├── hooks
│ │ ├── providers
│ │ ├── stores
│ │ ├── styles
│ │ │ ├── mixins.scss
│ │ │ ├── font.scss
│ │ │ ├── global.scss
│ │ │ ├── tinyReset.scss
│ │ ├── types
│ │ ├── utils
│ ├── home
│ │ ├── apis
│ │ ├── components
│ │ │ ├── mainBanner // src/common/components/mainBanner 가져와 사용
│ │ │ ├── quickLink // src/common/components/quickLink 가져와 사용
│ │ │ ├── index.tsx
│ │ ├── providers
│ │ ├── stores
│ ├── search
│ │ ├── apis
│ │ ├── components
│ │ │ ├── SearchHeader
│ │ │ │ ├── SearchHeader.module.scss
│ │ │ │ ├── index.tsx
│ │ │ ├── SearchHome
│ │ │ │ ├── SearchHome.module.scss
│ │ │ │ ├── index.tsx
│ │ │ ├── SearchResult
│ │ │ │ ├── ui
│ │ │ │ │ ├── SearchAll
│ │ │ │ │ │ ├── SearchAll.module.scss
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── SearchKeyword
│ │ │ │ │ │ ├── SearchKeyword.module.scss
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ ├── SearchNews
│ │ │ │ │ │ ├── SearchNews.module.scss
│ │ │ │ │ │ ├── index.tsx
│ │ │ │ ├── SearchResult.module.scss
│ │ │ │ ├── index.tsx
│ │ ├── constant
│ │ ├── hooks
│ │ ├── providers
│ │ ├── stores
│ │ ├── types
│ │ ├── utils
│ ├── subHome
│ │ ├── apis
│ │ ├── components
│ │ │ ├── mainBanner // src/common/components/mainBanner 가져와 사용
│ │ │ ├── quickLink // src/common/components/quickLink 가져와 사용
│ │ │ ├── index.tsx
│ │ ├── providers
│ │ ├── stores
├── stories
1차 리팩토링을 진행한 구조이다.
아톰이니 위젯이니 피쳐니 뭐니 어디에 둬야할지 고민해야하는 구조가 아닌
단순하게 검색이면 /src/search 안에 몽땅 때려넣는 도메인(컴포넌트) 단위로 정착하기로 하였다.
비록 태초마을st로 돌아왔지만
지나간 Atomic 디자인, FSD 레이어, 이 두 아키텍쳐에게 잘못은 없을 것이다.
명확한 기준도 없이 제대로 이해하지 못한 채 사용하고 싶은대로 사용한게 잘못이었을지도...?
우리가 정착한 구조 또한 누군가에겐 어려운 구조일 수 있다.
다만, 이 코드를 직접 만져야하는 팀원들이 함께 머리를 맞대어 고민한 끝에 도출해낸 구조이기 때문에
적어도 우리 팀에게 맞는 구조일 것이라 믿어 의심치 않는다.
'Study > React, next.js' 카테고리의 다른 글
[React] 관심사 분리하기 (1) | 2025.07.16 |
---|---|
[Next.js] public > images, fonts 트래픽(성능) 이슈 (Local fonts를 곁들인...) (0) | 2025.06.04 |
[Next.js] export const dynamic = 'force-dynamic' (0) | 2025.02.04 |
[Next.js] 사이드 프로젝트 초기 셋팅하기 (0) | 2024.05.27 |