hyunzsu 님의 상세페이지[8팀 현지수] Chapter 2-3. 관심사 분리와 폴더구조

과제 체크포인트

기본과제

목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기

  • 전역상태관리를 사용해서 상태를 분리하고 관리하는 방법에 대한 이해
  • Context API, Jotai, Zustand 등 상태관리 라이브러리 사용하기
  • FSD(Feature-Sliced Design)에 대한 이해
  • FSD를 통한 관심사의 분리에 대한 이해
  • 단일책임과 역할이란 무엇인가?
  • 관심사를 하나만 가지고 있는가?
  • 어디에 무엇을 넣어야 하는가?

체크포인트

  • 전역상태관리를 사용해서 상태를 분리하고 관리했나요?
  • Props Drilling을 최소화했나요?
  • shared 공통 컴포넌트를 분리했나요?
  • shared 공통 로직을 분리했나요?
  • entities를 중심으로 type을 정의하고 model을 분리했나요?
  • entities를 중심으로 ui를 분리했나요?
  • entities를 중심으로 api를 분리했나요?
  • feature를 중심으로 사용자행동(이벤트 처리)를 분리했나요?
  • feature를 중심으로 ui를 분리했나요?
  • feature를 중심으로 api를 분리했나요?
  • widget을 중심으로 데이터를 재사용가능한 형태로 분리했나요?

심화과제

목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기

  • TanstackQuery의 사용법에 대한 이해
  • TanstackQuery를 이용한 비동기 코드 작성에 대한 이해
  • 비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해

체크포인트

  • 모든 API 호출이 TanStack Query의 useQuery와 useMutation으로 대체되었는가?
  • 쿼리 키가 적절히 설정되었는가?
  • fetch와 useState가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가?
  • 캐싱과 리프레시 전략이 올바르게 구현되었는가?
  • 낙관적인 업데이트가 적용되었는가?
  • 에러 핸들링이 적절히 구현되었는가?
  • 서버 상태와 클라이언트 상태가 명확히 분리되었는가?
  • 코드가 간결하고 유지보수가 용이한 구조로 작성되었는가?
  • TanStack Query의 Devtools가 정상적으로 작동하는가?

최종과제

  • 폴더구조와 나의 멘탈모데일이 일치하나요?
  • 다른 사람이 봐도 이해하기 쉬운 구조인가요?

과제 셀프회고

https://hyunzsu.github.io/front_6th_chapter2-3/?limit=10&skip=0

이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요.

본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요?

1. FSD 아키텍처 구현

전체 폴더 구조
src/
├── entities/                 # 비즈니스 엔티티 (도메인 객체)
│   ├── comment/             
│   │   ├── api/             # 댓글 관련 API 호출
│   │   ├── constants/      
│   │   ├── lib/          
│   │   └── types/   
│   ├── post/                
│   │   ├── api/         
│   │   ├── constants/
│   │   ├── lib/             
│   │   └── types/      
│   ├── tag/               
│   │   ├── api/           
│   │   └── types/     
│   └── user/           
│       ├── api/          
│       └── types/    
├── features/                # 사용자 행동 중심 기능
│   ├── comment-management/   # 댓글 관리 도메인
│   │   ├── create/          # 댓글 생성 기능
│   │   │   └── api/      
│   │   ├── list/            # 댓글 목록 표시
│   │   │   └── ui/    
│   │   ├── interactions/    # 댓글 상호작용 (좋아요)
│   │   │   └── api/        
│   │   ├── shared/          # 댓글 공통 UI
│   │   │   └── ui/          
│   │   └── update/          # 댓글 수정/삭제
│   │       └── api/        
│   ├── post-management/     # 게시물 관리 도메인
│   │   ├── create/          # 게시물 생성
│   │   │   ├── api/       
│   │   │   └── ui/        
│   │   ├── list/            # 게시물 목록/검색/필터
│   │   │   ├── api/         
│   │   │   ├── hooks/    
│   │   │   ├── model/   
│   │   │   └── ui/         
│   │   ├── shared/          # 게시물 공통 UI  
│   │   │   └── ui/        
│   │   └── update/          # 게시물 수정/삭제
│   │       └── api/      
│   └── user-profile/        # 사용자 프로필 조회
│       └── ui/            
├── shared/                  # 공통 요소
│   ├── hooks/              
│   ├── lib/                
│   ├── types/          
│   └── ui/               
├── widgets/                 # 독립적 위젯
│   ├── header/           
│   └── footer/           
├── pages/                   # 페이지 조합
│   └── PostsManagerPage.tsx 
└── app/                     # 앱 레벨 설정
    └── Providers.tsx       
📍 Features 레이어 - 사용자 행동 중심 분리

Features 레이어 - 사용자 행동 중심 분리

전체 구조

features/
├── comment-management/     # 댓글과 관련된 모든 사용자 행동
├── post-management/       # 게시물과 관련된 모든 사용자 행동
└── user-profile/          # 사용자 정보 조회 행동

각 도메인별 내부 구조:

  • create/ - 새로운 데이터를 생성하는 행동
  • list/ - 데이터를 조회하고 표시하는 행동
  • interactions/ - 데이터와 상호작용하는 행동 (좋아요, 공유 등)
  • update/ - 기존 데이터를 수정/삭제하는 행동
  • shared/ - 해당 도메인 내에서 공통으로 사용되는 UI/로직

A. Post Management - 검색/필터링

폴더 구조 상세 분석

post-management/list/
├── api/                           # 복합 조회 API 집합
│   ├── listWithAuthorPostApi.ts          # 기본 목록 + 작성자
│   ├── searchWithAuthorPostApi.ts        # 검색 결과 + 작성자  
│   ├── tagWithAuthorPostApi.ts           # 태그 필터 + 작성자
│   ├── filterPostApi.ts                  # 순수 태그 필터
│   └── searchPostApi.ts                  # 순수 검색
├── hooks/                         # 커스텀 훅
│   └── usePostsUrlSync.ts                # URL 상태 동기화
├── model/                         # 상태 관리 (Jotai)
│   └── queryParams.ts                    # 모든 필터/검색 상태
└── ui/                           # UI 컴포넌트 분리
    ├── PostManagerList.tsx              # 최상위 컨테이너
    ├── SearchAndFilters.tsx             # 검색/필터 UI
    ├── PostTable.tsx                    # 테이블 표시
    ├── PostTableRow.tsx                 # 개별 행
    └── PaginationControls.tsx           # 페이지네이션

API 폴더 - Entities와 Features 분리 이유

과제에서 게시물 목록에 작성자 정보를 함께 표시해야 했습니다. API 응답 구조상 게시물과 사용자 정보가 분리되어 있어 데이터 조합이 필요했고, 데이터 조합 로직을 어디에 둘 것인지 고민이 많았습니다.

GET /posts → { posts: [{ id, title, body, userId, tags }], total }
GET /users → { users: [{ id, username, image }], total }

Entities 레이어: 순수한 도메인 API만

// entities/post/api/listPostApi.ts
export const fetchPosts = async (params) => {
  return api.get<PostsResponse>(`/posts?${queryParams}`)
}

// entities/user/api/listUserApi.ts  
export const fetchUsers = async (params) => {
  return api.get<UsersResponse>(`/users?${queryParams}`)
}

다른 feautres에서도 동일한 AP를 사용할 수 있도록 재사용성을 위해 단순하게 유지했습니다. 예를 들어 댓글 작성자 정보를 가져올 때도 동일한 fetchUsers API를 사용할 수 있고, 사용자 프로필 조회 기능에서도 같은 API를 활용할 수 있습니다.

Features 레이어: 비즈니스 로직 처리

// features/post-management/list/api/listWithAuthorPostApi.ts
const fetchPostsWithAuthors = async (params) => {
  // 1. 필요한 데이터를 병렬로 가져오기
  const [postsData, usersData] = await Promise.all([
    fetchPosts(params),                           // entities API 재사용
    fetchUsers({ limit: 0, select: "username,image" })  // entities API 재사용
  ])

  // 2. UI에서 필요한 형태로 데이터 조합
  const postsWithUsers = postsData.posts.map(post => ({
    ...post,
    author: usersData.users.find(user => user.id === post.userId)
  }))

  return { posts: postsWithUsers, total: postsData.total }
}

게시물 목록에서 작성자 이름과 이미지를 함께 표시하기 위해 features 레이어에 데이터 조합 로직을 위치시켰습니다. 이렇게 하면 UI 컴포넌트에서는 복잡한 데이터 처리 없이 필요한 정보를 바로 사용할 수 있습니다. 만약 나중에 게시물에 댓글 수 같은 추가 정보가 필요하다면, entities의 기본 API는 그대로 두고 features에서만 새로운 조합 로직을 추가하면 됩니다.

사용자 시나리오별 API 분리 게시물 목록에서 다양한 상황에 대응하기 위해 시나리오별로 API를 분리했습니다.

// 1. 일반 목록 조회
// features/post-management/list/api/listWithAuthorPostApi.ts
const fetchPostsWithAuthors = async (params) => {
  const [postsData, usersData] = await Promise.all([
    fetchPosts(params),              // GET /posts
    fetchUsers({ limit: 0 })
  ])
  // 데이터 조합...
}

// 2. 검색 결과 조회
// features/post-management/list/api/searchWithAuthorPostApi.ts  
const fetchSearchPostsWithAuthors = async (query) => {
  const [searchResults, usersData] = await Promise.all([
    fetchSearchPosts({ query }),     // GET /posts/search
    fetchUsers({ limit: 0 })
  ])
  // 데이터 조합...
}

// 3. 태그별 조회
// features/post-management/list/api/tagWithAuthorPostApi.ts
const fetchPostsByTagWithAuthors = async (tag) => {
  const [tagResults, usersData] = await Promise.all([
    fetchPostsByTag({ tag }),        // GET /posts/tag/{tag}
    fetchUsers({ limit: 0 })
  ])
  // 데이터 조합...
}

일반 목록, 검색 결과, 태그별 조회가 각각 독립적인 캐시 키를 가지므로 사용자가 검색하다가 태그를 클릭해도 기존 데이터가 유지됩니다. 또한 검색어가 없거나 태그가 선택되지 않았을 때는 해당 API를 호출하지 않도록 조건부 실행을 적용해서 불필요한 네트워크 요청을 방지했습니다.


Model 폴더 - 전역 상태 관리 설계

게시물 목록 화면에서 사용자는 검색어 입력, 태그 선택, 정렬 기준 변경, 페이지 이동 등 다양한 상호작용을 합니다. 이 모든 상태를 어떻게 관리하고, URL과 동기화할 것인지 고민이 많았습니다. 전역 상태 관리 라이브러리로 Jotai를 선택했습니다. Props Drilling을 제거할 수 있고, 각 컴포넌트가 필요한 상태만 구독해서 최소한의 리렌더링을 보장하기 때문입니다.

상태 설계: 도메인별 분리

// features/post-management/list/model/queryParams.ts

// 1. 사용자 입력 상태
export const postsSearchQueryAtom = atom("")           // 검색어
export const postsSelectedTagAtom = atom("all")        // 선택된 태그
export const postsSortByAtom = atom("none")            // 정렬 기준
export const postsSortOrderAtom = atom<"asc" | "desc">("asc")  // 정렬 순서

// 2. 페이지네이션 상태
export const postsPageAtom = atom(1)                   // 현재 페이지
export const postsLimitAtom = atom(10)                 // 페이지당 항목 수
export const postsSkipAtom = atom(0)                   // 건너뛸 항목 수

상태를 사용자 입력과 페이지네이션으로 분리해서 각 atom이 명확한 역할을 갖도록 설계했습니다. 이렇게 세분화하면 컴포넌트에서 관심있는 상태만 구독할 수 있어 불필요한 리렌더링을 방지할 수 있습니다. 또 각 상태의 변경 영향 범위를 예측하기 쉬워져 유지보수하기 용이해집니다.

URL 자동 동기화

// 3. 파생 상태로 URL 쿼리스트링 자동 생성
export const postsQueryStringAtom = atom((get) => {
  const limit = get(postsLimitAtom)
  const skip = get(postsSkipAtom)
  const searchQuery = get(postsSearchQueryAtom)
  const selectedTag = get(postsSelectedTagAtom)
  const sortBy = get(postsSortByAtom)
  const sortOrder = get(postsSortOrderAtom)

  const params = { limit: String(limit), skip: String(skip) }
  
  if (searchQuery.trim()) params.search = searchQuery
  if (selectedTag && selectedTag !== "all") params.tag = selectedTag
  if (sortBy && sortBy !== "none") params.sortBy = sortBy
  if (sortOrder !== "asc") params.sortOrder = sortOrder

  return new URLSearchParams(params).toString()
})

사용자의 모든 필터 상태를 URL 쿼리스트링으로 자동 변환하는 파생 상태를 구현했습니다. 이를 통해 사용자가 새로고침해도 현재 필터 상태가 유지되고, URL을 복사해서 공유하면 동일한 화면을 볼 수 있습니다.

Jotai의 액션 패턴 활용

// 복잡한 상태 변경을 하나의 액션으로 처리
export const setPostsPageAtom = atom(null, (get, set, page: number) => {
  const limit = get(postsLimitAtom)
  set(postsPageAtom, page)
  set(postsSkipAtom, (page - 1) * limit)  // skip 값 자동 계산
})

export const setPostsLimitAtom = atom(null, (get, set, limit: number) => {
  const page = get(postsPageAtom)
  set(postsLimitAtom, limit)
  set(postsSkipAtom, (page - 1) * limit)  // 현재 페이지 기준으로 skip 재계산
})

페이지 변경 시 skip 값도 함께 업데이트해야 하는 로직을 액션 패턴으로 해결했습니다. 여러 상태를 동기화해서 변경하는 로직을 한 곳에 집중시켜서 일관성을 보장하고, 컴포넌트에서는 단순히 setPage(2) 호출만 하면 되도록 구성했습니다.

실제 사용 예시: 컴포넌트 간 상태 공유

// 검색 컴포넌트
export const SearchInput = () => {
  const [searchQuery, setSearchQuery] = useAtom(postsSearchQueryAtom)
  const [, setPage] = useAtom(setPostsPageAtom)

  const handleSearchChange = (value: string) => {
    setSearchQuery(value)  // 검색어 변경
    setPage(1)             // 첫 페이지로 자동 이동
  }
  // ...
}

// 필터 컴포넌트
export const FiltersContainer = () => {
  const [selectedTag, setSelectedTag] = useAtom(postsSelectedTagAtom)
  const [, setPage] = useAtom(setPostsPageAtom)

  const handleTagChange = (tag: string) => {
    setSelectedTag(tag)    // 태그 변경
    setPage(1)             // 첫 페이지로 자동 이동
  }
  // ...
}

// 목록 컴포넌트 - 모든 상태를 읽기만 함
export const PostManagerList = () => {
  const searchQuery = useAtomValue(postsSearchQueryAtom)
  const selectedTag = useAtomValue(postsSelectedTagAtom)
  const limit = useAtomValue(postsLimitAtom)
  const skip = useAtomValue(postsSkipAtom)
  
  // 상태 조합해서 적절한 API 호출
  // ...
}

B. Post Management - shared 폴더 설계

전역 shared vs 도메인 shared 구분

shared/ui/ (전역 공통 - 모든 도메인에서 사용)
├── Button.tsx        # 범용 버튼
├── Dialog.tsx        # 범용 다이얼로그
├── Input.tsx         # 범용 입력
└── Table.tsx         # 범용 테이블

post-management/shared/ui/ (게시물 도메인 전용)
├── PostFormDialog.tsx     # 게시물 생성/수정 폼
└── PostDetailDialog.tsx   # 게시물 상세보기

PostFormDialog - 생성/수정 공통 폼 설계

// features/post-management/shared/ui/PostFormDialog.tsx

interface PostFormDialogProps {
  mode: "create" | "update"    // 모드에 따른 동작 분기
  isOpen: boolean
  onClose: () => void
  post?: PostWithAuthor | null // 수정 모드일 때만 필요
}

export const PostFormDialog = ({ mode, isOpen, onClose, post }) => {
  // 각 기능의 API Hook 사용
  const { mutate: createPost } = useCreatePost()    // create/api
  const { mutate: updatePost } = useUpdatePost()    // update/api

  // 폼 상태 관리
  const [title, setTitle] = useState("")
  const [body, setBody] = useState("")
  const [userId, setUserId] = useState(1)

  // 모드에 따른 폼 초기화
  useEffect(() => {
    if (mode === "update" && post) {
      setTitle(post.title)
      setBody(post.body)
      setUserId(post.userId)
    } else {
      setTitle("")
      setBody("")
      setUserId(1)
    }
  }, [mode, post, isOpen])

  // 모드에 따른 제출 로직 분기
  const handleSubmit = () => {
    if (!title.trim() || !body.trim()) return

    if (mode === "create") {
      createPost(
        { title: title.trim(), body: body.trim(), userId },
        { onSuccess: () => handleClose() }
      )
    } else if (mode === "update" && post) {
      updatePost(
        { id: post.id, data: { title: title.trim(), body: body.trim() } },
        { onSuccess: () => handleClose() }
      )
    }
  }

  return (
    <Dialog open={isOpen} onOpenChange={onClose}>
      <DialogContent>
        <DialogTitle>
          {mode === "create" ? "새 게시물 추가" : "게시물 수정"}
        </DialogTitle>
        
        <div className="space-y-4">
          <Input 
            placeholder="제목" 
            value={title} 
            onChange={(e) => setTitle(e.target.value)} 
          />
          
          <Textarea
            rows={mode === "create" ? 30 : 15}
            placeholder="내용"
            value={body}
            onChange={(e) => setBody(e.target.value)}
          />
          
          {/* 생성 모드일 때만 사용자 ID 입력 */}
          {mode === "create" && (
            <Input
              type="number"
              placeholder="사용자 ID"
              value={userId}
              onChange={(e) => setUserId(Number(e.target.value))}
            />
          )}
          
          <div className="flex gap-2 justify-end">
            <Button variant="outline" onClick={onClose}>
              취소
            </Button>
            <Button onClick={handleSubmit} disabled={!title.trim() || !body.trim()}>
              {mode === "create" ? "게시물 추가" : "게시물 업데이트"}
            </Button>
          </div>
        </div>
      </DialogContent>
    </Dialog>
  )
}

다양한 기능에서 공통 사용

// 1. 게시물 생성에서 사용
// features/post-management/create/ui/CreateButton.tsx
export const CreateButton = () => {
  const { isModalOpen, handleModalOpen, handleModalClose } = useModal()

  return (
    <>
      <Button onClick={handleModalOpen}>
        <Plus className="w-4 h-4 mr-2" />
        게시물 추가
      </Button>
      
      {isModalOpen && (
        <PostFormDialog 
          mode="create" 
          isOpen={isModalOpen} 
          onClose={handleModalClose} 
        />
      )}
    </>
  )
}

// 2. 게시물 수정에서 사용  
// features/post-management/list/ui/PostControlPanel.tsx
export const PostControlPanel = ({ post }) => {
  const { isModalOpen, handleModalOpen, handleModalClose } = useModal()

  return (
    <>
      <Button onClick={handleModalOpen}>
        <Edit2 className="w-4 h-4" />
      </Button>
      
      {isModalOpen && (
        <PostFormDialog 
          mode="update" 
          isOpen={isModalOpen} 
          onClose={handleModalClose} 
          post={post}
        />
      )}
    </>
  )
}

PostDetailDialog - 상세보기 + 댓글 통합

// features/post-management/shared/ui/PostDetailDialog.tsx
export const PostDetailDialog = ({ isOpen, onClose, post }) => {
  // 검색어 상태를 사용해서 하이라이팅
  const searchQuery = useAtomValue(postsSearchQueryAtom)

  // 검색어 하이라이팅 함수
  const renderHighlightedText = (text: string, query: string) => {
    const { parts, isMatch } = splitTextForHighlight(text, query)
    return (
      <span>
        {parts.map((part, i) => 
          isMatch(part) ? <mark key={i}>{part}</mark> : part
        )}
      </span>
    )
  }

  return (
    <Dialog open={isOpen} onOpenChange={onClose}>
      <DialogContent className="max-w-3xl">
        <DialogHeader>
          <DialogTitle>
            {renderHighlightedText(post.title, searchQuery)}
          </DialogTitle>
        </DialogHeader>
        
        <div className="space-y-4">
          <p>{renderHighlightedText(post.body, searchQuery)}</p>

          {/* 댓글 위젯 통합 - 다른 feature의 컴포넌트 사용 */}
          <CommentList postId={post.id} searchQuery={searchQuery} />
        </div>
      </DialogContent>
    </Dialog>
  )
}

아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요.

이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요.

챕터 셀프회고

클린코드와 아키테쳑 챕터 함께 하느라 고생 많으셨습니다! 지난 3주간의 여정을 돌이켜 볼 수 있도록 준비해보았습니다. 아래에 적힌 질문들은 추억(?)을 회상할 수 있도록 도와주려고 만든 질문이며, 꼭 질문에 대한 대답이 아니어도 좋으니 내가 느꼈던 인사이트들을 자유롭게 적어주세요.

클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기

  • 더티코드를 접했을 때 어떤 기분이었나요? ^^; 클린코드의 중요성, 읽기 좋은 코드란 무엇인지, 유지보수하기 쉬운 코드란 무엇인지에 대한 생각을 공유해주세요

결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리

  • 거대한 단일 컴포넌트를 봤을때의 느낌! 처음엔 막막했던 상태관리, 디자인 패턴이라는 말이 어렵게만 느껴졌던 시절, 순수함수로 분리하면서 "아하!"했던 순간, 컴포넌트가 독립적이 되어가는 과정에서의 깨달음을 들려주세요

응집도 높이기: 서버상태관리, 폴더 구조

  • "이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간, FSD를 적용해보면서의 느낌, 나만의 구조를 만들어가는 과정, TanStack Query로 서버 상태를 분리하면서 느낀 해방감(?)등을 공유해주세요

리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문

과제 피드백

수고했습니다. 지난 3주간 클린코드를 비롯한 소프트웨어 공학적으로 결합도 낮추기 응집도 높이기를 위한 이론과 프론트엔드에서의 적용등을 통해서 좋은 코드와 구조에 대한 다각도의 시야가 생겼기를 기대합니다.

"현재 댓글과 게시물의 데이터 조합 로직을 features 레이어에 배치했지만, 이 결정이 완전히 명확한지 확신이 서지 않습니다. 예를 들어 fetchPostsWithAuthors는 두 개의 entities API를 조합하는 역할인데, 이것이 features에 있어야 하는지 아니면 별도의 레이어나 shared 영역에 있어야 하는지 고민됩니다. 특히 다른 기능에서도 동일한 조합이 필요할 때 중복 코드가 발생할 가능성이 있습니다."

사실 굉장히 헷갈리는 문제죠. 특히나 layers의 단위가 의존성을 바탕으로 수직적인 계층을 다루는 만큼 복수개의 entities를 다루게 되면 features인가? 라고 생각하게 되고 실제로 그런 경우가 많으니까요.

그래서 해당 코드를 features로 넣게 되면 다른 features와 성격이 유사한가 더 이상 이 코드는 entities가 아니게 되나? 이런 생각을 한 번해보세요. entities끼리도 계층과 의존이 있고 features끼리도 계층과 의존이 있을 수 있는데 의존과 계층이 아니라 둘이 분명 갈라지게 하는 캐릭터가 존재하고 그게 달라지는 경계를 기준으로 삼아보기를 바래요.

과제를 통해서 전달하고 싶은 내용들이 잘 전달이 된것 같네요. 이 코드를 어디에 둘까를 고민하는 것 자체에 좋은 코드를 만드는 생각의 시작이기에 좋은 계기가 되어주었기를 바랍니다.

Q) FSD 아키텍처 레이어 분리 기준 데이터 조합 로직 위치: fetchPostsWithAuthors 같은 복합 API를 features에 두었는데, 다른 기능에서도 같은 조합이 필요할 때 중복을 어떻게 해결해야 할까요? entities vs features 판단 기준이 궁금합니다.

features 내 shared 폴더: features/post-management/shared/ui/PostFormDialog.tsx에 생성/수정 공통 폼을 배치했습니다. 이렇게 특정 도메인 내에서만 공유되는 컴포넌트를 위해 features 안에 shared 폴더를 만드는 것이 FSD 표준에 부합하는지, 아니면 다른 구조가 더 적절한지 궁금합니다.

=> 앞서 설명했지만 entities vs features는 조합의 문제가 아니라 실제로 분리를 해보면서 작업을 하면 명확히 다른 그룹이 형성이 됩니다. 엔티티를 사용하는게 기준이 아니라 성격이 달라지는 것들을 기준으로 생각해보면 도움이 될거에요.

Q) React Query 캐시 키 쿼리 키 팩토리 패턴: 현재 쿼리 키가 하드코딩되어 있어서 오타나 불일치 위험이 있습니다. queryKeys.posts.withAuthors() 같은 쿼리 키 팩토리 패턴을 도입하는 것이 프로젝트 규모에 비해 과도한 엔지니어링인지, 아니면 초기부터 적용하는 것이 좋은지 의견을 듣고 싶습니다.

=> 이건 제가 이렇다라고 답을 드린들 큰 의미가 없는것 같아요. 제가 설명하는 언어가 그걸 다 세세하게 말할 수 있을 만큼 설명하기가 어렵다고 생각해요. 그런 생각이 들 때에는 그냥 해보는게 좋습니다. 그래서 쿼리 키 팩토리 패턴이 정말로 도움이 되는지 그렇지 않은지를 느껴보고 나면 답을 찾을 수 있을 거에요!

=> 개인적은 선호는 쿼리 키 팩토리를 굳이 만들려고 하지는 않는 편입니다. 대개 디버깅을 api를 기준으로 찾게 되는데 쿼리 키 팩토리가 api를 쉽게 찾을 수 있게 해주지는 않더라구요. 그렇지만 이 개념이 등장하는데에는 이유가 있겠죠? 찬성파는 찬성하니까 만들어지는 것이니 꼭 본인이 다 경험해서 판단하기를 바래요 :)

Q) Jotai atom 분리 기준 atom 세분화: 현재 게시물 필터링 상태를 7개 atom으로 나누었습니다. 이렇게 세분화하면 컴포넌트가 필요한 상태만 구독해서 불필요한 리렌더링을 방지할 수 있지만, atom 개수가 많아져서 관리가 복잡합니다. 실무에서는 리렌더링 최적화와 코드 복잡성 사이에서 어떤 기준으로 판단하나요?

성능 vs 복잡성: 리렌더링 최적화를 위해 atom을 세분화했지만 관리 복잡도가 증가했습니다. 실무에서는 어떤 기준으로 균형을 잡는지 궁금해요!

=> 문제가 없다면 간결한 쪽이 낫습니다. FE는 성능보다 관리가 더 중요하니까요. 방치하라는건 아니고 최대한 적은 코드도 간결하게 구현할 수 있는 방향으로 구현하되 문제가 생길때 성능의 이슈가 생길때 복잡성을 조금씩 구현하는 방향이 좋습니다. 복잡도가 높아지는게 좋은 코드는 아니니까요.

수고많았습니다!!