Skip to content

[7팀 김민규] Chapter 2-3. 관심사 분리와 폴더구조#65

Open
suinkimme wants to merge 32 commits intohanghae-plus:mainfrom
suinkimme:main
Open

[7팀 김민규] Chapter 2-3. 관심사 분리와 폴더구조#65
suinkimme wants to merge 32 commits intohanghae-plus:mainfrom
suinkimme:main

Conversation

@suinkimme
Copy link

@suinkimme suinkimme commented May 1, 2025

과제 체크포인트

https://suinkimme.github.io/front_5th_chapter2-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가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가?
  • 캐싱과 리프레시 전략이 올바르게 구현되었는가?

과제 셀프회고

image

과제에서 좋았던 부분

React 프로젝트를 진행하면서 FSD(Feature-Sliced Design) 관점에서 애플리케이션 구조를 바라보게 된 점이 인상 깊었습니다. 단순히 컴포넌트를 나열하는 수준을 넘어서, 기능 단위로 책임을 분리하고 각 계층의 역할을 명확히 하려는 시도가 개발 전반에 걸쳐 큰 영향을 주었습니다.

처음에는 구조를 강제하는 것이 오히려 개발 속도를 저하시킬 수 있지 않을까 우려도 있었지만, 오히려 반대로 기능이 복잡해질수록 이러한 설계적 사고가 코드의 유지보수성과 확장성을 높이는 데 기여한다는 점을 체감할 수 있었습니다. 특히, entities, features, shared, widgets, pages 등 각 층위에서 어떤 책임을 가져가야 하는지 고민하면서 자연스럽게 "이 기능은 어디에 있어야 맞는가?"라는 질문을 습관화할 수 있었습니다.

또한 이 과정에서 디렉터리 구조를 의미 있게 설계하는 법, 컴포넌트 간의 결합도를 낮추는 방법, 단방향 데이터 흐름을 유지하면서도 기능을 나누는 방법 등에 대해 실제적인 감을 잡을 수 있었던 것도 큰 수확이었습니다.

이번 프로젝트를 계기로 단순히 돌아가는 코드가 아니라, 잘 구조화된 코드, 앞으로도 손을 대기 쉬운 코드를 만들기 위해 어떤 기준이 필요한지를 더 깊이 고민하게 되었습니다. 앞으로의 프로젝트에서도 이러한 설계 관점을 적극적으로 적용해보고 싶다는 생각이 들었습니다.

과제를 하면서 새롭게 알게된 점

이번 과제를 하면서 기술적인 내용보다 제 습관이나 태도에서 부족한 점을 더 많이 느꼈습니다. 특히 정리 정돈이 잘 안 된다는 점이 계속 드러났습니다. 기능을 구현하려고 할 때, 미리 정리해 둔 게 없다 보니 혼자 헤매는 시간이 많았습니다.

처음부터 계획 없이 시작했던 탓에 구조를 바꾸거나 코드를 다시 손보는 데 불필요한 시간이 들어갔습니다. 그동안 ‘일단 되게 만들자’는 식으로 넘겼던 습관들이 결과적으로는 비효율로 이어졌다는 걸 깨달았습니다.

앞으로는 개발을 시작하기 전에 흐름이나 구조를 미리 정리하고, 작업 중간에도 계속 기록을 남기는 습관이 필요하겠다고 느꼈습니다. 그래야 같은 문제를 반복하지 않을 수 있을 것 같습니다.

과제를 진행하면서 아직 애매하게 잘 모르겠다 하는 점, 혹은 뭔가 잘 안되서 아쉬운 것들

  • 전역 상태를 관리하는 store를 feature에 두는 게 맞는지 잘 모르겠습니다. 또, jotai나 Context API로는 어떻게 처리해야 할지도 아직 잘 모르겠습니다.
  • React Custom Hook이 아직 익숙하지 않아서 여전히 머리가 복잡합니다.
  • TanStack Query를 적용하면서 상태를 직접 관리하지 않게 되니, 추가나 수정, 삭제 후에 UI에 변화가 있어야 하는 부분이 제대로 구현되지 않았습니다. 이런 흐름이 맞는 건지, 아니면 다른 방식으로 처리해야 하는 건지 잘 모르겠습니다. 처음 써보는 라이브러리라 혼란스러운 부분이 있었고, 더 공부가 필요하겠다는 생각을 했습니다.

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

  • 현재 전역 상태를 feature 디렉터리 내부에 두었는데, 이 위치가 적절한지 잘 모르겠습니다. 상태 범위와 의존성을 고려했을 때 더 적절한 구조나 위치가 있다면 조언 부탁드립니다.
  • Jotai와 Context API를 사용할 때, 상태를 어떻게 분리하고 공유 범위를 설정해야 관리가 쉬운지 잘 감이 안 잡힙니다. 실무 기준에서 어떤 방식이 유지보수에 효과적인지 피드백 주시면 감사하겠습니다.
  • Custom Hook을 작성하긴 했지만, 관심사 분리가 잘 되었는지 확신이 없습니다. 이 Hook이 너무 많은 책임을 가지고 있는 건 아닌지, 적절히 분리된 건지 확인 부탁드립니다.
  • 현재 Custom Hook 내부에서 zustand store를 사용하고 있습니다. 이렇게 사용하는 방식이 관심사 분리나 재사용성 측면에서 적절한지, 구조적으로 문제가 없는지 검토해주실 수 있을까요?
  • 2년 차 개발자이지만, 이전 직장에서 FE 팀을 리드하며 프로젝트 일정 조율과 기획에도 다수 참여한 경험이 있습니다. 특히, 디자인 시스템과 GitHub, Jira, Figma를 팀에 도입하면서 회사의 협업 방식에 실질적인 변화를 주었습니다. 현재도 이 도구들이 잘 사용되고 있다는 점은 재직 중인 동료를 통해 확인할 수 있었습니다. 제가 경험한 이런 부분들이 이력서에 어필할 수 있는 요소인지 궁금합니다. 제 입장에서는 큰 변화라고 느꼈지만, 실무에서는 당연하게 여겨지는 수준인지 잘 판단이 되지 않습니다.

export const COMMENT_QUERIES = {
all: ["comments"] as const,
byPostId: (postId: number) => [...COMMENT_QUERIES.all, "post", postId] as const,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tanstack-query 공식문서에서 추천하는 query key factory 라이브러리도 한 번 추천드려봅니다.

외관적으로는 달라지는 부분은 크게 없지만, 개인적으로는 타이핑이 강화되고 자동완성이 지원돼서 사용할 때 더 안정감이 생기는 정도(?) 입니다 ㅎㅎ.

return useMutation({
mutationFn: (comment: INewComment) => commentApi.createComment(comment),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: COMMENT_QUERIES.byPostId(variables.postId) })
Copy link

@jotace06 jotace06 May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invalidate을 통해 새로 호출하는 것으로 이게 되는거였나? 하는 생각이 들어 확인해봤는데 msw api를 통해서 서버 작업을 구현하셨군요. 멋집니다 👍

setShowAddCommentDialog: (showAddCommentDialog: boolean) => void
setShowEditCommentDialog: (showEditCommentDialog: boolean) => void
setSelectedComment: (selectedComment: IComment | null) => void
}
Copy link

@jotace06 jotace06 May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

하나로 관리하는게 코드 중복도 줄이고 가독성도 좋아보이네요. 배워갑니다 👍

onClick={() => {
updateComment.mutate(selectedComment)
setShowEditCommentDialog(false)
setSelectedComment(null)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기의 일련의 동작도 useCommentEditForm hook 에서 조립해서 export 하는 건 어떻게 생각하시나요? interaction과 관련된 event handler는 의도적으로 따로 관리하시는건지 궁금해서 여쭤봅니다 ㅎㅎ.

title: "",
body: "",
userId: 1,
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작성중인 post 내용은 local state로 분리하는 꼼꼼함...! 성능에도 신경을 쓰셨군요 👍

const setLimit = usePostStore((state) => state.setLimit)
const setSkip = usePostStore((state) => state.setSkip)
const setTotal = usePostStore((state) => state.setTotal)
const setSelectedPost = usePostStore((state) => state.setSelectedPost)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 사실 처음에 아래처럼 사용하다가 무한 렌더링의 늪에 빠져서...

const { ... } = usePostStore(state => ({
  limit: state.limit,
  skip: state.skip,
  ...
}))

useShallow 라는 memoizedSelector 라는걸 알게 됐습니다.

export { default as PostDetailModal } from "./PostDetailModal"
export { default as PostCreateModal } from "./PostCreateModal"
export { default as PostManagerTitle } from "./PostManagerTitle"
export { default as PostEditModal } from "./PostEditModal"
Copy link

@jotace06 jotace06 May 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 개인적인 convention으로 index.ts 를 통해서 export할 함수컴포넌트들을 전부 관리하는데, 모든 파일에대해서 named export 로 통일하니까 index.ts 에서는 늘 export * from "/..." 로 줄일 수 있어서 편한 것 같더라구요 ㅎㅎ.

return <button className={buttonVariants({ variant, size, className })} ref={ref} {...props} />
})

Button.displayName = "Button"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"baseUrl": "src",
"paths": {
"@/*": ["*"]
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작은 차이지만, import 문 가독성과 편의성에서 확연한 차이가 만들어지네요. 참고하겠습니다.

Copy link

@jotace06 jotace06 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다!
전반적으로 코드가 깔끔하고 응집도 좋게 나뉘어 있어서 쉽게 파악할 수 있었어요. 다른 사람에게 잘 읽히는 코드를 작성하기 위해 많은 노력을 기울여주셨다는게 느껴졌습니다. 민규님 코드를 보면서 앗 나도 이렇게 할 걸! 하는 부분을 여러곳 발견하게 됐어요. 특히 msw로 유사 서버기능을 구현해서 dummyjson api의 부족한 부분까지 채워주신 점도 너무 멋지네요. 잘 읽고 공부해 갑니다. 👍

Comment on lines +26 to +30
<UserModal />
<PostCreateModal />
<PostEditModal />
<PostDetailModal />
<CommentCreateModal />

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 보통 pages에 많이 위치시키던데 모달들을 widgets에 위치시키셨군요! widgets에 위치시킨 이유가 궁금합니닷

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그저... 테오가 모달은 위젯이다 라는 말을 어디서 주워들었습니다.

Comment on lines +1 to +8
import { usePostModalStore } from "@/features/post/model/store"

export const usePostModal = () => {
const showPostModal = usePostModalStore((state) => state.showPostModal)
const setShowPostModal = usePostModalStore((state) => state.setShowPostModal)

return { showPostModal, setShowPostModal }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modal 상태를 전역적으로 관리하면서 또 커스텀 훅을 만드신 이유가 있나용?

Copy link

@jinsoul75 jinsoul75 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

시간없었다면서!! 역시 해내는 민규님!!!!! 짱짱!!!!!! 정글 가지마요 흑흑흑흑흑흐긓그흑흑흐긓그흑 못보내!!!

Comment on lines +8 to +22
async function main() {
await worker.start({
serviceWorker: {
url: "/front_5th_chapter2-3/mockServiceWorker.js",
},
})

ReactDOM.createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryProvider>
<App />
</QueryProvider>
</StrictMode>,
)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이제 ES2022 버전부터는 Top-level await가 지원되어서, 더 이상 async function main() { ... }과 같이 await 코드를 main 함수로 래핑하지 않아도 괜찮답니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아앗..! 그렇군요!! AI를 사용했더니.. 이런 부작용이...ㅠㅠ

Comment on lines +34 to +36
export const Button = forwardRef<HTMLButtonElement, IButtonProps>(({ className, variant, size, ...props }, ref) => {
return <button className={buttonVariants({ variant, size, className })} ref={ref} {...props} />
})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이번 과제의 React 버전이 19라서, 몇몇 경우에는 더 이상 forwardRef를 명시적으로 사용하지 않아도 된다고 합니다! 코치님께서 이번 업데이트를 의도하신 것은 아니라고 말씀하셨지만, 이 점을 참고하시면 코드를 조금 더 간결하게 작성할 수 있는 부분이 있을 것 같아요.

Copy link
Author

@suinkimme suinkimme May 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아~~ 그렇구나... 사실 저는 forwardRef가 뭔지 잘 몰라서 공부해볼 필요가 있을 것 같아요!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토스에서 useOverlay라는 Overlay를 선언적으로 관리하기 위한 유틸 훅을 만들었어요! 저는 zustand와 스까스까서 활용하고 있답니다. 토스는 Overlay를 어떻게 관리하는지 한번 보시면 좋으실것 같아서 남겨놓습니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오...! 감사합니다!

Copy link

@keyonnaise keyonnaise left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

민규님, 크래프톤 정글 과정으로 매우 바쁘실 텐데도 과제를 훌륭하게 완료하신 것을 보니 저도 정말 기쁩니다. 크래프톤 정글 과정 또한 훌륭히 마치실 수 있을 거라고 믿습니다! 항상 응원하겠습니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants