-
Notifications
You must be signed in to change notification settings - Fork 46
[5팀 진재윤] Chapter 3-3 기능 중심 아키텍처와 프로젝트 폴더 구조 #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jy0813
wants to merge
28
commits into
hanghae-plus:main
Choose a base branch
from
jy0813:main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
전체 디렉토리 구조
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
첫째, 관심사 분리. "이 데이터가 뭐지?"와 "이 데이터를 어떻게 바꾸지?"는 다른 질문이다.
둘째, FSD 원칙 준수. FSD 공식 문서에서 features를 "비즈니스 가치를 주는 액션"이라고 정의한다. 데이터를 바꾸는 건 확실히 액션이다.
셋째, 의존성 방향. features가 entities를 import하는 건 자연스러운데, 반대는 이상하다.
게시물 삭제 버튼이 게시물 데이터를 알아야 하는 건 당연한데, 게시물 데이터가 삭제 버튼을 알 필요 없다.
API는 왜 entities에만 설계했나? Create, Update, Delete API 도 있는데, 위에 Query와 Mutation을 생각하면 features에 들어가야하는것이 아닌가?
API 함수 자체는 그냥 "서버랑 통신하는 인프라"다. 진짜 "변화"는 useMutation Hook에서 일어난다.
그래서 API 함수(postApi.ts)는 모두 entities에 두면서, useMutation만 features로 분리하는 게 핵심이었습니다. API 함수 자체는 "도구"일 뿐이고, 그 도구를 "어떻게 사용하는가"(Mutation + 캐시 무효화)가 진짜 "행동"이기 때문입니다.
SOLID 원칙을 배울 때 "의존성 역전"이라는 말을 들었습니다. 하지만 솔직히 뭔 소린지 이해를 못했고, "인터페이스에 의존해라", "추상화에 의존해라"... 말은 알겠는데 실제로 어떻게 쓰는 건지 감이 안 왔습니다.
이번 과제에서 FSD 아키텍처를 적용하면서 진짜 의존성 역전이 필요한 상황을 마주쳤습니다.
앞서, FSD 아키텍처의 핵심은 "계층 간 단방향 의존성" 입니다. 그래서 ESLint의
no-restricted-imports규칙으로 컴파일 타임에 강제하기로 했습니다.이를 지키기 위해 적용한 게 의존성 역전 패턴 입니다.
이 패턴을 적용하고 나서야 "아, 의존성 역전이 이거구나!"를 체감했습니다.
이게 바로 "상위 레이어가 하위 레이어의 인터페이스에 의존한다"는 의미이고, 추상적으로 느껴졌는데, 실제로 적용해보니 왜 이런 패턴이 필요한지 이해가 됐습니다.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
708줄짜리 PostsManagerPage.tsx를 한 번에 리팩토링하는 대신, Phase 1부터 7까지 단계별로 진행하며 각 단계마다 ESLint 검증을 수행했습니다. 덕분에 중간에 문제가 생겨도 어느 단계에서 발생했는지 쉽게 파악할 수 있었습니다.
FSD 의존성 방향을 철저히 지키려고 노력했습니다.
이 방향을 거스르는 import가 발생하면 즉시 수정하고, ESLint 규칙으로 자동 검증되도록 설정했습니다. 특히 highlightText 함수의 위치를 entities/post/lib에서 shared/lib로 옮긴 것처럼, 여러 도메인에서 사용되는 코드는 적절한 레이어로 이동시키려고 노력했습니다.
또한, AI 대본 작성을 통해 개념을 내재화하려고 했습니다. 단순히 코드를 작성하는 것에서 그치지 않고, "사수가 부사수에게 설명하는 대본", "팀 토론 대본", "YAGNI에 대한 솔직한 대화" 등을 작성하면서 왜 이렇게 하는지를 스스로 설명할 수 있는 수준까지 이해하려고 노력했습니다.
대본 링크
기존코드의 sortOrder=asc Params는 잘못된 코드 => order 로 변경하여 정렬 버그 수정 dummyjson
기존코드는 게시글 수정 시 작성자가 깨지는 이슈 수정
아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
"지금 필요한 것만 만들자"와 "나중을 위해 구조를 잡아두자" 사이에서 어디까지가 적절한지 판단하기 어려웠습니다.
"비용이 낮으면 해도 괜찮다"는 기준이 있지만, 실제 프로젝트에서 이 기준을 적용하려면 더 많은 경험이 필요할 것 같습니다.
이번에는 기존의 과제들과 다르게 테스트 코드가 없어서, 직접 작성해볼까도 고민해봤는데, 구현 내용만 우선 제대로 동작하는지
테스트 하기 위해서 playwright mcp 를 사용하여 E2E 테스트 밖에 진행하지 못했습니다.
FSD는 레이어가 잘 분리되어 있어서 각 레이어를 독립적으로 테스트할 수 있을텐데, 모든 레이어를 모두 테스트해야 하는지, 아니면 우선순위가 있는지 이런게 가장 큰 고민입니다.
순수함수로 분리만 해놓고 테스트를 하나도 못 했다는 점에서 많이 아쉽고, 앞으로 이 부분을 확실히 보완하고 싶다는 생각이 들었습니다.
이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
이번에 배운 내용을 통해 앞으로 개발을 할 때, 기능을 ‘막연하게 구현’하는 것이 아니라 각 단계를 더 명확하게 구조화해서 접근하는 습관을 적용해보고 싶습니다.
기능을 만들 때 가장 먼저 “이 기능의 데이터는 무엇인가?”부터 정의하고, 그 데이터를 entities 레이어의 model/types.ts에 정리하는 방식으로
도메인 중심의 개발 흐름을 의식적으로 가져가려고 합니다.
그 다음으로 “이 데이터를 서버로부터 어떻게 가져올까?”라는 관점에서 필요한 API들을 entities/api에 분리하고, 조회(Read) 기능은 entities/model/useXxxQuery.ts에서 관리해 데이터 접근 로직을 UI와 확실히 분리하는 구조를 적용할 계획입니다.
반대로 생성/수정/삭제(CUD) 액션은 feature 단위에서 발생하는 사용자 의도이기 때문에 features/model/useXxxMutation.ts에서 관리하며, 사용자 인터랙션 UI는 features/ui 쪽으로 모아 “사용자 액션 단위”로 슬라이스를 구성하는 방식도 실전에서 사용해보고 싶습니다.
여러 feature와 entity를 조합해 화면을 구성해야 하는 경우에는 이를 widgets/ui로 올려 좀 더 재사용 가능한 조합 단위로 구성함으로써,
pages 레이어에서는 가능한 한 화면 배치와 라우팅에만 집중하는 구조를 유지하려고 합니다.
정리하자면, 앞으로는 기능을 구현할 때마다 ‘데이터 정의 → API → 조회 → 변경 → 액션 UI → 조합’이라는 일련의 흐름을
FSD 구조에 맞게 체계적으로 나눠서 개발하며, 기능을 더 명확하게 분리하고 유지보수하기 쉬운 형태로 작성하는 것을 목표로 하고 있습니다.
챕터 셀프회고
이번 챕터를 진행하면서 가장 크게 느낀 점은 “좋은 구조를 만들기 위해서는 좋은 분석이 먼저 필요하다”는 것입니다.
클린코드를 지향하거나, 결합도를 낮추고 응집도를 높이며, 순수함수/컴포넌트 분리/상태 관리/폴더 구조를 잘 가져가는 것이 결국 목표였지만,
이 모든 것은 as-is를 정확하게 파악하지 못하면 적용할 수 없다는 걸 다시 깨달았습니다.
기능을 구현하기 전에는 항상 이렇게 스스로에게 질문해야 합니다.
이 질문들에 명확하게 답할 수 있어야 그 다음에야 비로소 FSD 구조든, 디자인 패턴이든, 클린한 컴포넌트 구조든 “적용할 때 고민 없이” 자연스럽게 결정할 수 있다는 걸 느꼈습니다.
결론은 좋은 코드 구조는 분석의 결과라고 생각합니다. 테오의 말대로 그 모습이 상당히 유니크하고 독창적이지 않았고, 다른 사람들도 이해하기 쉬운 코드 였습니다.
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
어디서부터 건드려야 하지?
처음 Before 패키지를 분석했을 때, 가장 먼저 든 감정은 혼란감이었습니다.
같은 컴포넌트 내부에서 인라인 스타일 + CSS 클래스 + 하드코딩 색상이 뒤섞여 있었고 UI 컴포넌트가 도메인 로직을 내부에서 판단하고 있었으며, 하나의 페이지가 648줄짜리 God Component로 모든 상태, 모든 로직을 다 들고 있었기 때문입니다.
특히 스타일링 같은 경우에는 인라인 스타일이 컴포넌트 곳곳에 뿌려져 있어, 눈으로 일일이 찾아가며 수정해야 했습니다.
“이 변경이 다른 곳에 영향을 줄까?”라는 불안함도 따라다녔습니다. 더티코드의 가장 큰 문제는 **“코드를 수정하는 순간 확신이 없어진다”**는 점입니다. 읽히지 않는 코드 안에서는 작은 수정조차도 에러가 되곤 했습니다.
그렇다면 읽기 좋은 코드란 무엇일까?
이번 챕터를 통해 저는 “읽기 좋은 코드”의 기준을 다시 생각하게 되었습니다.
Before에는 button.tsx 안에서 UI Props와 도메인 로직이 섞여 있었지만, After에서는 UI는 UI만 비즈니스 로직은 훅으로,
디자인 설정은 토큰으로 명확히 분리되었습니다.
이렇게 관심사가 분리되면 파일을 열자마자 **"아, 얘는 이런 역할을 하는구나"**를 바로 이해할 수 있습니다.
Before 코드에서는 특정 색상이나 spacing 값이 여러 파일에 하드코딩 되어 있어 변경 시 모든 컴포넌트를 직접 찾아 수정해야 했지만,
After에서는 디자인 토큰을 만들고 Tailwind + CVA 기반으로 일관된 스타일 시스템을 도입했습니다. 유지보수가 쉬워지고 의도도 명확해지고
컴포넌트 사용법도 직관적입니다. “UI는 적어도 스타일 때문에 고생하면 안 된다”는 것을 깨달았습니다.
리팩토링을 통해 648줄짜리 ManagementPage가 137줄로 분리되었고, 각각의 훅이 단일 책임만 담당하게 되었습니다.
이렇게 나누고 나니 코드는 읽기 쉬워졌을 뿐 아니라, 변경도 훨씬 안전해졌습니다.
유지보수하기 좋은 코드란 무엇일까?
3-1 작업을 통해 저는 유지보수성이란 단순히 “버그가 없고 튼튼한 코드”가 아니라, 변경 비용을 최소화할 수 있는 구조라고 생각하게 되었습니다.
Before 에는 로직과 UI가 섞여 있어 수정하면 사이드 이펙트가 불명확하고, 도메인 규칙이 곳곳에 산재해 있어 검색이 힘들고,
컴포넌트 구조가 일관성이 없어 새 기능 추가가 항상 모험이었습니다.
After 에는 폴더 구조가 역할 기준으로 재정리되고 컴포넌트가 UI-only 역할을 하며 토큰 기반 디자인 시스템으로 변경 영향도가 줄어들었고
상태와 액션이 훅으로 분리되며 테스트 가능성도 올라갔습니다. 결국 “예측 가능한 코드”가 유지보수하기 좋은 코드라는 것을 명확히 느꼈습니다.
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
상태를 어디에 둬야 하는가?
예를 들어 searchTerm(검색어)을 다룰 때, HeaderWidget에서 입력하고 ProductListWidget에서 사용해야 하는데
두 컴포넌트가 서로 연결되어 있지 않았습니다.
처음엔 “전역으로 올리면 끝 아닌가?”라고 생각했지만 곰곰이 따져보니 검색어는 쇼핑 페이지 안에서만 의미가 있는 값이었고, 앱 전체에서 필요한 전역 상태라고 보기 어려웠습니다.
그래서 검색어는 App에서만 관리하고 props로 내려보내는 방식을 선택했고, 이 과정에서 처음으로 전역 상태의 편의성 vs props의 명시성
이라는 설계적 균형을 처음 체감했습니다.
그 결과, 나름의 기준도 생겼습니다. "3단계 이내의 전달이면 props, 그 이상이면 전역"
단순한 기술적 선택이 아니라, “왜 그렇게 해야 하는지”를 스스로 따져본 결정이었습니다.
설계적 사고가 자리 잡기 시작하다.
이 경험을 거치면서 저는 점점 상태를 이렇게 구분하게 됐습니다.
도메인 데이터와 관련 있다 → Zustand 같은 전역 상태 라이브러리 사용
예: Product, Cart, User 등 여러 페이지에서 사용되고 서버 상태와 동기화되며 앱의 핵심 데이터(domain)에 해당한다.
따라서 전역/비즈니스 상태는 Zustand로 관리하는 게 자연스럽다.
UI 상태이며 스코프가 넓다 → Context로 명확한 범위를 지정
예: Toast, Modal, Theme, 도메인과 직접적 연관은 없지만 여러 컴포넌트에서 사용하고 UI적으로 “광역 범위”가 필요한 값들 이런 값은 Context/Provider로 해결하는 게 더 적절하다.
searchTerm은? 과제가 끝나고 현재 3번째 챕터를 겪어보니, 처음엔 Zustand에 올릴까 고민했지만, 보통 검색어는 **queryParams(주소)**와 동기화하는 게 일반적이라는 걸 알게 됐습니다. 그래야 새로고침해도 유지되고, URL 공유도 가능하기 때문입니다.
즉 searchTerm은 실제로는 상태 관리보다 라우팅 컨텍스트에 더 가깝다라는 정리가 되면서 "상태를 어디에 둬야 하는가?"에 대한 기준이 생겼고, 상태가 담고 있는 의미(도메인 vs UI vs 라우팅)에 기반한 설계적 사고의 틀이 잡히기 시작했습니다.
순수함수 분리 여정
상태 관리 기준이 잡힌 뒤, 다음 혼란은 “로직을 어디에 둘 것인가?”였습니다. 처음에는 컴포넌트 안에 모든 계산 로직과 조건문이 들어가 있었는데 이걸 분리하기 시작하면서 또 하나의 깨달음(?)을 느꼈습니다. 예를 들어 getCartSummary() 같은 순수함수는 입력만 같으면 항상 같은 결과를 주기 때문에 테스트하기 쉽고 어디서 호출해도 안정적이고 UI 로직과 비즈니스 로직을 명확히 분리할 수 있었습니다.
이걸 경험하면서 “UI는 UI만, 계산은 계산만, 서버 통신은 서버만” 이라는 설계 원리를 몸으로 이해하게 되었습니다. 함수형 프로그래밍, 아토믹 디자인, 데이터 흐름 분리 같은 개념들이 처음으로 “그래서 이런 걸 하는구나”로 바뀌었습니다.
설계적 사고가 생기면서 결합도가 자연스럽게 낮아진다.
처음엔 전역 상태냐 props냐, 이런 문제들로만 고민했는데 이제는 상태가 가진 “의미"를 기준으로 판단하게 되었습니다.
이 값은 도메인인가? 이 값은 UI인가? 이 값은 어느 범위에서 의미가 생기는가? 이 로직은 계산인가, 액션인가?
이 함수는 어디에 있어야 가장 결합도가 낮아지는가? 이런 질문을 하면서 설계적 사고 → 자연스러운 구조 분리 → 낮은 결합도 라는 흐름이 몸에 잡히기 시작했습니다. 결과적으로 단순히 "파일을 나누는" 게 아니라 “코드를 왜 그렇게 설계해야 하는지”를 이해한 게 이번 챕터에서 가장 큰 성장 포인트였습니다.
응집도 높이기: 서버상태관리, 폴더 구조
"이 함수는 어디에 둬야 하지?"라는 질문을 던지는 것 자체가 응집도를 높이는 과정 입니다.
질문을 던지면 자연스럽게 답이 나옵니다.
이 사고 과정을 반복하다 보니, 코드를 작성하기 전에 "이건 어디에 속하는 코드지?"를 먼저 생각하는 습관이 생겼습니다.
TanStack Query로 서버 상태를 분리하면서 느낀 해방감
이 패턴의 문제점:
posts,loading,error를 직접 관리isLoading,error를 useState로 관리 안 해도 됨결국.. TanstackQuery도 관심사의 분리만 잘하면, 서버 상태와 UI 상태의 명확한 분리가 됩니다.
나만의 폴더 및 레이어 계층 기준
레이어 계층
간단한 폴더 구조 (맨 위 전체 구조 참고)
entities/*/api/entities/*/model/types.tsentities/*/model/use*Query.tsfeatures/*/model/use*Mutation.tsentities/*/lib/*.tsfeatures/*/ui/*.tsxwidgets/*/ui/*.tsxshared/lib/,shared/ui/리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
PR 을 작성하면서 저 또한 개념과 학습한것들에 대하여 정리가 많이 되는거 같지만, 그래도 한번 더 확인하고 싶어서 질문드립니다. 회고에 작성한 내용들이 제가 이번 챕터에서 배워가야할 것들을 제대로 짚고 넘어가고있는걸까요?
레이어 계층은 나름의 기준과 다른 사람이 봐도 이해하기 쉬운 구조로 작성했다고 생각하고 있습니다. 코치님이 제가 올려놓은 전체 디렉토리 구조 를 보고 내부의 파일들이 각각 알맞게 잘 적용된건지 피드백을 주실수 있을까요?