[1팀 이은지] Chapter 2-3. 관심사 분리와 폴더구조#48
Conversation
✨ 주요 변경사항: - FSD 아키텍처 원칙에 따른 레이어 구조 정리 - Features 레이어에 posts-list 분리 및 엔티티 간 의존성 제거 - PostWithAuthor 조합 타입을 Features 레이어로 이동 - Tag 엔티티 추가 및 관련 API/쿼리 구현 🔧 기술적 개선: - ESLint 설정을 Airbnb 스타일로 업그레이드 - Import 경로를 절대 경로로 정리 및 Public API 활용 - 타입 시스템 개선 및 중복 export 제거 - 빈 폴더 제거 및 파일 구조 정리
| import { useState } from 'react'; | ||
|
|
||
| import { useCreateCommentMutation } from '@/entities/comment'; | ||
| import { useUIStore } from '@/shared/lib'; |
There was a problem hiding this comment.
UI스토어를 보니 모달과 선택된 포스트 그리고 코멘트 등이 같이 존재하는데 이 부분은 각각의 스토어로 분리하면 더 깔끔했을것같아요..!
그렇게 분리하고 모달을 여는 기능훅을 만들고 주입해서 사용하는것도 좋은 방법같더라구요..! (물론 저도 그렇게 안함..)
| }; | ||
|
|
||
| useEffect(() => { | ||
| setCommentBody(selectedComment?.body || ''); |
There was a problem hiding this comment.
selectComment 함수를 이 훅에서 정의하고, 그 함수 내부에서 setCommentBody를 하게 하는건 혹시 어떻게 생각하시나요..? 저는 사이드는 최소화 해야한다는 생각에 그렇게 하긴했는데 은지님 의견이 궁금합니다..!
|
|
||
| import { useAddCommentForm } from '../hooks/useAddCommentForm'; | ||
|
|
||
| export const AddCommentDialog = () => { |
There was a problem hiding this comment.
UI 컴포넌트를 어떻게 분리해야할지 막막했는데 이렇게 보니 뭔가 좀 느낌이 옵니다!!
| ...[Object.values(filters)], | ||
| ]); | ||
|
|
||
| const postsWithAuthorBySearch = useMemo(() => { |
There was a problem hiding this comment.
재 계산 안되도록 메모 한거 너무좋아요.... 백엔드는 왜 조인 안해서 주냐고...
| if (searchQuery && searchQuery.trim()) { | ||
| setPagination({ | ||
| ...pagination, | ||
| total: postsBySearchData?.total ?? 0, |
There was a problem hiding this comment.
토탈은 페이지네이션 상태로 두지 않고, 파생값으로 둬도 될것같아요! 이 자체가 하나의 상태인데 다른 상태에 옮겨간 형태같습니다..!
| ...pagination, | ||
| total: postsData?.total ?? 0, | ||
| }); | ||
| }, [postsBySearchData, postsByTagData, postsData]); |
There was a problem hiding this comment.
이건 궁금증인데, searchQuery값이 의존성배열에 없으면 첫 번째 if문은 실행이 어떻게 되는거에요..?
| }, | ||
| onSuccess: (data, _, { previousPosts }) => { | ||
| // ! 성공 시 이전 데이터에 성공 데이터 추가 | ||
| queryClient.setQueryData(cachedPostsQueryKey, (old: any) => { |
There was a problem hiding this comment.
이미 onMutate 에서 진행했던 사항을 똑같이 진행하는것 같아서 불필요해 보입니다!
| return { | ||
| showAddDialog, | ||
| setShowAddDialog, | ||
| showEditDialog, | ||
| setShowEditDialog, | ||
| showAddCommentDialog, | ||
| setShowAddCommentDialog, | ||
| showEditCommentDialog, | ||
| setShowEditCommentDialog, | ||
| showPostDetailDialog, | ||
| setShowPostDetailDialog, | ||
| showUserModal, | ||
| setShowUserModal, | ||
| selectedPost, | ||
| setSelectedPost, | ||
| selectedComment, | ||
| setSelectedComment, | ||
| selectedUser, | ||
| setSelectedUser, | ||
| }; |
There was a problem hiding this comment.
소소한 제안입니다, 인터페이스를 이렇게 제공하면 어떨까요?
const { addDialog } = useUIStore();
addDialog.isOpen;
addDialog.open();
addDialog.clpse();
| @@ -0,0 +1,21 @@ | |||
| export const highlightText = (text: string, highlight: string) => { | |||
There was a problem hiding this comment.
이거 리액트 엘리먼트를 반환하는 함수인데 그냥 컴포넌트로 바꿔보는 건 어떨까요?
export const HighlightText = ({ text, highlight }: { text: string; highlight: string; }) => {| export const createPost = async (newPost: Omit<Post, 'id'>): Promise<Post> => { | ||
| return ApiService.post<Post>('/posts/add', newPost); | ||
| }; |
There was a problem hiding this comment.
공통된 내용인데, ApiService.post 에 이미 제네릭으로 Post 가 반환 타입임을 적용했으므로 래핑한 함수에서는 굳이 명시하지 않아도 알 수 있을 것 같아서 간단하게 이렇게만 적어도 괜찮을 것 같아요!
| export const createPost = async (newPost: Omit<Post, 'id'>): Promise<Post> => { | |
| return ApiService.post<Post>('/posts/add', newPost); | |
| }; | |
| export const createPost = async (newPost: Omit<Post, 'id'>) => { | |
| return ApiService.post<Post>('/posts/add', newPost); | |
| }; |
배포 링크
https://angielxx.github.io/front_6th_chapter2-3
과제 체크포인트
기본과제
목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기
체크포인트
심화과제
목표: 서버상태관리 도구인 TanStack Query를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기
체크포인트
최종과제
과제 셀프회고
이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.
FSD를 처음 적용할 때 가장 인상적이었던 건 "어디에 무엇을 넣어야 할지"가 명확해진다는 점이었습니다. 이전에는 새로운 기능을 추가할 때마다 "이 코드는 어느 폴더에 넣지?"라고 고민했는데, FSD를 적용하고 나니 자연스럽게 적절한 위치가 보이더라구요.
이렇게 각 레이어가 명확한 역할을 가지니까 코드를 찾기도 쉽고, 새로운 팀원이 와도 구조를 이해하기 쉬울 것 같았습니다.
하지만! 이런 장점을 체감하려면 FSD에 대해 정확히 이해하고 있고, 어느 정도 FSD에 익숙한 상태여야 가능합니다.
FSD 구조하에 모듈을 어디에 위치시킬지에 대한 zep 토론 흔적..
primitive 컴포넌트를 기능별로 분류하여 더 쉽게 찾아서 쓸 수 있도록 했습니다.
헤더,푸터의 경우 shared에 포함되는 primitive 컴포넌트보다 더 넓은 범위의 컴포넌트라고 생각해서 widget에 두었습니다. shared 내의 코드들은 어느 상황의 프로젝트에서도 사용할 수 있으만한 공통 모듈을 넣으려고 했습니다. 또한 의존성 부분에서도 pages에서만 사용되기 때문에 widget에 두는 것이 더 적절하다고 생각했습니다!
FSD 구조를 기반으로 모듈을 분리하고 위치시키면서 각 모듈에 대한 기능, 역할에 대해 더 얇은 레이어로(?) 디테일하게 생각해볼 수 있었던 것 같습니다. 기존 모듈 역할 단위로만 나누던 폴더 구조에서는 모듈의 분류에 대해 깊이 생각하지 않기 때문에 프로젝트가 커질 경우 모듈이 서로 뒤섞이게 되는 것 같습니다.
낙관적 업데이트를 적용하기 전에는 댓글을 추가하거나 좋아요를 누를 때마다 서버 응답을 기다려야 했습니다. 사용자 입장에서는 "내가 뭔가 했는데 반응이 없네?"라는 답답함이 있었어요.
하지만 낙관적 업데이트를 적용하고 나니 버튼을 누르는 순간 즉시 UI가 업데이트되면서 훨씬 반응성 있는 앱이 되었습니다. "아, 이래서 요즘 앱들이 이렇게 빠르게 느껴지는구나"라고 깨달았어요.
하지만 낙관적 업데이트를 구현하면서 예상보다 복잡한 부분이 많았습니다. 성공했을 때는 문제없지만, 실패했을 때 이전 상태로 롤백하는 로직이 생각보다 까다로웠어요.
본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?
1. FSD 아키텍처의 완벽한 이해와 적용
각 레이어의 역할 명확히 정의하기
FSD를 단순히 폴더 구조로만 이해하지 않고, 각 레이어가 왜 존재하는지, 어떤 책임을 가져야 하는지 깊이 고민했습니다.
이런 식으로 같은 엔티티 기반으로 파일명을 가독성이 높게 파일명을 작성하려고 했습니다. 가독성 뿐만 아니라 엔티티 하위에 다중 폴더 구조가 되지 않게 관련 코드끼리 모아뒀습니다.
import 할 때 어느 슬라이스에 포함되는 모듈인지 명시적으로 확인할 수 있도록 layer 최상단에서는 index.ts로 노출시키지 않고 각 slice에서만 index.ts로 export 했습니다.
2. 제네릭을 활용한 타입 안전성
타입 정의 시 타입을 통해 데이터의 구조나 기능이 상상이 가능할 수 있도록 실제 기능과 구조를 잘 표현하기 위해 노력했습니다.
예를들어, 사용자 목록 조회 시 'select' 파라미터를 통해 사용자 모델의 속성을 선택할 수 있게 되어있습니다. 이 기능을 온전히 타입에 표현되도록 제네릭 타입을 사용하여 표현했습니다.
API 응답에서 필요한 속성만 선택적으로 가져올 수 있도록 제네릭을 활용하고 타입 안정성을 보장하여 런타임 에러를 방지할 수 있었습니다.
유틸리티 타입을 사용하여 단순 타입(?) 형태로 표현하지 못하는 경우들을 표현할 수 있도록 했습니다.
명시적이고 의도가 명확하게 타입을 정의하려고 했고, 재사용 가능한 유틸리티 타입으로 일관성 있는 타입 시스템 구축하고자 했습니다.
3. 쿼리키 관리
Work in progress... 더 작성하겠습니다. 죄송합니다..
4. FSD 폴더 구조 사용을 위한 개발자 경험을 향상시키는 도구와 설정
FSD 폴더 구조에서는 Layer 계층이 분명하고 단방향 의존성을 지켜야하기 때문에 그 계층이 시각적으로 눈에 보여서 어느 폴더가 상위이고 하위인지 생각하는 시간을 줄이고자 했습니다.
FSD의 최상위 폴더, 즉 Layer의 상위 ~ 하위 계층은
이런 순서로 구성이 되는데 만약 이 폴더를 그대로 만들게 되면 알파벳 순서로 정렬되기 때문에
이렇게 뒤섞여보이기 때문에 의존성 방향을 시각적으로 바로 확인하기가 어려웠습니다.
그래서 폴더 순서가 의존성 계층 순서와 일치하여 폴더 위치만 봐도 의존성 관게를 파악할 수 있도록 폴더명 앞에 순서를 붙였습니다.
하지만 이 폴더명 그대로 import문에 지저분하게 노출하여 사용하고 싶진 않았습니다. 그래서 절대 경로 설정을 통해 import 문에서 숫자없이 Layer 명만 깔끔하게 노출하여 사용할 수 있게 했습니다. 그리고 절대 경로 입력시 각 레이어가 몇번 폴더인지 생각할 필요없이 바로 Layer명으로 자동 완성할 수 있기 때문에 개발 생산성을 높일 수 있었습니다
이렇게 절대경로를 실제 숫자가 붙은 폴더명을 Layer명으로 alias하고 싶었습니다
그래서 절대 경로 설정은 아래와 같이 설정했습니다.
처음에 가장 최상단 폴더에 대한 절대 경로 설정을 맨 위에 두었더니, 다른 모든 절대경로를 인식하지 못하는 문제가 있었습니다
문제가 발생한 설정
이렇게 설정했을 때는
이런 경로가 에러가 발생하게 됩니다!
'@': path.resolve(__dirname, './src'),여기에서 먼저 걸러지기 때문에‘@’ 하위의 정확한 폴더명을 적어줘야 정상적으로 작동하게 됩니다.
그래서 alias되는 절대 경로를 우선 처리하고 나머지 절대 경로를 인식할 수 있도록 ‘@’를 최하단에 두었더니 모든 절대 경로가 정상적으로 동작했습니다!!
아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.
이번에 배운 내용을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.
챕터 셀프회고
클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기
결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리
응집도 높이기: 서버상태관리, 폴더 구조
리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문
질문 1: FSD 아키텍처에서 도메인 중심 슬라이스 설계의 적절성
FSD 아키텍처에서 도메인 중심의 슬라이스 사용이 적절한지, 그리고 현재 구조를 개선할 수 있는 더 나은 방법이 있는지 궁금합니다.
현재
src/final_src/4_features/post-management경로에서 게시물 관련 기능들을 하나의 도메인 중심 슬라이스로 구성하고 있습니다.현재 접근 방식의 배경:
게시물 기능을 CRUD 단위로 세분화하면 post-create, post-read, post-update, post-delete와 같은 개별 슬라이스들이 생성되는데, 각각이 너무 작은 단위가 되어 오히려 관리 복잡도가 증가할 수 있다고 판단했습니다. 그래서 현재는 post-management라는 도메인 중심의 상위 개념으로 묶어서 관리하고 있습니다.
검토하고 싶은 부분:
코드위치:
src/final_src/4_features/post-management
질문 2: 낙관적 업데이트의 선언적 모듈화 방안
낙관적 업데이트 로직을 선언적이고 재사용 가능한 형태로 추상화하여 코드 중복을 줄이고 유지보수성을 향상시키고 싶습니다.
문제 상황:
현재 React Query의 useMutation에서 낙관적 업데이트를 onMutate, onError, onSuccess 콜백들을 통해 명령형으로 구현하고 있습니다. 이로 인해 다음과 같은 문제들이 발생하고 있습니다:
구체적인 문제점:
코드위치:
src/final_src/5_entities/post/post.queries.ts 의 useCreatePostMutation (46번째 줄)
질문 3: 다중 쿼리 타입 환경에서의 캐시 동기화 전략
게시물에 변경사항(생성, 수정, 삭제)이 발생할 때, 현재 활성화된 쿼리 타입을 감지하여 해당하는 쿼리키로 캐시된 데이터에만 낙관적 업데이트를 수행하도록 구현했습니다. 이러한 방식이 적절한지, 더 나은 방법은 없는지 궁금합니다.
실무에서 이러한 상황이 발생했을 때 확장성, 유지보수성을 고려하여 세련되게 처리할 수 있는 방법이 없을까요?
현재 상황:
게시물 목록을 조회하는 방식이 세 가지 타입(기본 목록, 태그별 필터링, 검색어 기반)으로 나뉘어 있으며, 각각 다른 API 엔드포인트에서 데이터를 가져옵니다.
코드위치: