diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index aeb47f408..123d064de 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -3,6 +3,7 @@ ### 기본과제 #### 목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기 + - 전역상태관리를 사용해서 상태를 분리하고 관리하는 방법에 대한 이해 - Context API, Jotai, Zustand 등 상태관리 라이브러리 사용하기 - FSD(Feature-Sliced Design)에 대한 이해 @@ -12,6 +13,7 @@ - 어디에 무엇을 넣어야 하는가? #### 체크포인트 + - [ ] 전역상태관리를 사용해서 상태를 분리하고 관리했나요? - [ ] Props Drilling을 최소화했나요? - [ ] shared 공통 컴포넌트를 분리했나요? @@ -24,10 +26,9 @@ - [ ] feature를 중심으로 api를 분리했나요? - [ ] widget을 중심으로 데이터를 재사용가능한 형태로 분리했나요? - ### 심화과제 -#### 목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기 +#### 목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기 - TanstackQuery의 사용법에 대한 이해 - TanstackQuery를 이용한 비동기 코드 작성에 대한 이해 @@ -45,8 +46,8 @@ - [ ] 코드가 간결하고 유지보수가 용이한 구조로 작성되었는가? - [ ] TanStack Query의 Devtools가 정상적으로 작동하는가? - ### 최종과제 + - [ ] 폴더구조와 나의 멘탈모데일이 일치하나요? - [ ] 다른 사람이 봐도 이해하기 쉬운 구조인가요? @@ -60,7 +61,6 @@ ### 이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요. - ## 챕터 셀프회고 > 클린코드와 아키테쳑 챕터 함께 하느라 고생 많으셨습니다! @@ -68,15 +68,19 @@ > 아래에 적힌 질문들은 추억(?)을 회상할 수 있도록 도와주려고 만든 질문이며, 꼭 질문에 대한 대답이 아니어도 좋으니 내가 느꼈던 인사이트들을 자유롭게 적어주세요. ### 클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기 + - 더티코드를 접했을 때 어떤 기분이었나요? ^^; 클린코드의 중요성, 읽기 좋은 코드란 무엇인지, 유지보수하기 쉬운 코드란 무엇인지에 대한 생각을 공유해주세요 ### 결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리 + - 거대한 단일 컴포넌트를 봤을때의 느낌! 처음엔 막막했던 상태관리, 디자인 패턴이라는 말이 어렵게만 느껴졌던 시절, 순수함수로 분리하면서 "아하!"했던 순간, 컴포넌트가 독립적이 되어가는 과정에서의 깨달음을 들려주세요 ### 응집도 높이기: 서버상태관리, 폴더 구조 + - "이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간, FSD를 적용해보면서의 느낌, 나만의 구조를 만들어가는 과정, TanStack Query로 서버 상태를 분리하면서 느낀 해방감(?)등을 공유해주세요 +## 리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 +``` -## 리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 -``` \ No newline at end of file +``` diff --git a/.gitignore b/.gitignore index a547bf36d..4e26de91f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +.cursor/ \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 000000000..2312dc587 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..89b8e3841 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,93 @@ +# Dependencies +node_modules/ +pnpm-lock.yaml +package-lock.json +yarn.lock + +# Build outputs +dist/ +build/ +.next/ +out/ + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +jspm_packages/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Temporary folders +tmp/ +temp/ diff --git a/.prettierrc b/.prettierrc index d9ae6b1fb..2948310b8 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,9 +1,32 @@ { - "semi": false, - "printWidth": 120, + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, "tabWidth": 2, - "singleQuote": false, - "quoteProps": "consistent", - "trailingComma": "all", - "singleAttributePerLine": false -} \ No newline at end of file + "useTabs": false, + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "endOfLine": "lf", + "quoteProps": "as-needed", + "jsxSingleQuote": true, + "proseWrap": "preserve", + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "importOrder": [ + "^react$", + "^react-router-dom$", + "^lucide-react$", + "^[a-z]", + "", + "^@/", + "^[./]", + "^.*\\u0000$" + ], + "importOrderSeparation": true, + "importOrderSortSpecifiers": true, + "importOrderGroupNamespaceSpecifiers": true, + "importOrderCaseInsensitive": true, + "importOrderTypeScriptVersion": "4.4.0", + "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"] +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..37f481a25 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "bradlc.vscode-tailwindcss", + "ms-vscode.vscode-typescript-next", + "formulahendry.auto-rename-tag", + "christian-kohler.path-intellisense", + "ms-vscode.vscode-json" + ] +} diff --git a/PR.md b/PR.md new file mode 100644 index 000000000..b688312d8 --- /dev/null +++ b/PR.md @@ -0,0 +1,97 @@ +## 배포링크 + +https://angielxx.github.io/front_6th_chapter2-3 + +## 과제 체크포인트 + +### 기본과제 + +#### 목표 : 전역상태관리를 이용한 적절한 분리와 계층에 대한 이해를 통한 FSD 폴더 구조 적용하기 + +- 전역상태관리를 사용해서 상태를 분리하고 관리하는 방법에 대한 이해 +- Context API, Jotai, Zustand 등 상태관리 라이브러리 사용하기 +- FSD(Feature-Sliced Design)에 대한 이해 +- FSD를 통한 관심사의 분리에 대한 이해 +- 단일책임과 역할이란 무엇인가? +- 관심사를 하나만 가지고 있는가? +- 어디에 무엇을 넣어야 하는가? + +#### 체크포인트 + +- [x] 전역상태관리를 사용해서 상태를 분리하고 관리했나요? +- [x] Props Drilling을 최소화했나요? +- [x] shared 공통 컴포넌트를 분리했나요? +- [x] shared 공통 로직을 분리했나요? +- [x] entities를 중심으로 type을 정의하고 model을 분리했나요? +- [x] entities를 중심으로 ui를 분리했나요? +- [x] entities를 중심으로 api를 분리했나요? +- [x] feature를 중심으로 사용자행동(이벤트 처리)를 분리했나요? +- [x] feature를 중심으로 ui를 분리했나요? +- [x] feature를 중심으로 api를 분리했나요? +- [x] widget을 중심으로 데이터를 재사용가능한 형태로 분리했나요? + +### 심화과제 + +#### 목표: 서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기 + +- TanstackQuery의 사용법에 대한 이해 +- TanstackQuery를 이용한 비동기 코드 작성에 대한 이해 +- 비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해 + +#### 체크포인트 + +- [x] 모든 API 호출이 TanStack Query의 useQuery와 useMutation으로 대체되었는가? +- [x] 쿼리 키가 적절히 설정되었는가? +- [x] fetch와 useState가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가? +- [x] 캐싱과 리프레시 전략이 올바르게 구현되었는가? +- [x] 낙관적인 업데이트가 적용되었는가? +- [x] 에러 핸들링이 적절히 구현되었는가? +- [x] 서버 상태와 클라이언트 상태가 명확히 분리되었는가? +- [x] 코드가 간결하고 유지보수가 용이한 구조로 작성되었는가? +- [x] TanStack Query의 Devtools가 정상적으로 작동하는가? + +### 최종과제 + +- [x] 폴더구조와 나의 멘탈모데일이 일치하나요? +- [x] 다른 사람이 봐도 이해하기 쉬운 구조인가요? + +## 과제 셀프회고 + +### 이번 과제를 통해 이전에 비해 새롭게 알게 된 점이 있다면 적어주세요. + +### 본인이 과제를 하면서 가장 애쓰려고 노력했던 부분은 무엇인가요? + +- FSD 사용하기 편한 폴더명과 절대경로 설정 +- 타입안정성 +- 낙관적 업데이트 + +### 아직은 막연하다거나 더 고민이 필요한 부분을 적어주세요. + +- 낙관적 업데이트 최적화 및 공통화 +- FSD 폴더구조 + +### 이번에 배운 내용 중을 통해 앞으로 개발에 어떻게 적용해보고 싶은지 적어주세요. + +## 챕터 셀프회고 + +> 클린코드와 아키테쳑 챕터 함께 하느라 고생 많으셨습니다! +> 지난 3주간의 여정을 돌이켜 볼 수 있도록 준비해보았습니다. +> 아래에 적힌 질문들은 추억(?)을 회상할 수 있도록 도와주려고 만든 질문이며, 꼭 질문에 대한 대답이 아니어도 좋으니 내가 느꼈던 인사이트들을 자유롭게 적어주세요. + +### 클린코드: 읽기 좋고 유지보수하기 좋은 코드 만들기 + +- 더티코드를 접했을 때 어떤 기분이었나요? ^^; 클린코드의 중요성, 읽기 좋은 코드란 무엇인지, 유지보수하기 쉬운 코드란 무엇인지에 대한 생각을 공유해주세요 + +### 결합도 낮추기: 디자인 패턴, 순수함수, 컴포넌트 분리, 전역상태 관리 + +- 거대한 단일 컴포넌트를 봤을때의 느낌! 처음엔 막막했던 상태관리, 디자인 패턴이라는 말이 어렵게만 느껴졌던 시절, 순수함수로 분리하면서 "아하!"했던 순간, 컴포넌트가 독립적이 되어가는 과정에서의 깨달음을 들려주세요 + +### 응집도 높이기: 서버상태관리, 폴더 구조 + +- "이 코드는 대체 어디에 둬야 하지?"라고 고민했던 시간, FSD를 적용해보면서의 느낌, 나만의 구조를 만들어가는 과정, TanStack Query로 서버 상태를 분리하면서 느낀 해방감(?)등을 공유해주세요 + +## 리뷰 받고 싶은 내용이나 궁금한 것에 대한 질문 + +``` + +``` diff --git a/README.md b/README.md index 236bcf258..a0ba7be5b 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,12 @@ - FSD(Feature-Sliced Design)에 대한 이해 - FSD를 통한 관심사의 분리에 대한 이해 - - 단일책임과 역할이란 무엇인가? - - 관심사를 하나만 가지고 있는가? - - 어디에 무엇을 넣어야 하는가? - +- 단일책임과 역할이란 무엇인가? +- 관심사를 하나만 가지고 있는가? +- 어디에 무엇을 넣어야 하는가? 체크포인트 + - [ ] 전역상태관리를 사용해서 상태를 분리하고 관리했나요? - [ ] Props Drilling을 최소화했나요? - [ ] shared 공통 컴포넌트를 분리했나요? @@ -54,8 +54,6 @@ - [ ] widget을 중심으로 데이터를 재사용가능한 형태로 분리했나요? ``` - - ## [6주차] 심화과제 여러분들은 비동기 코드가 들어가고 서버와 통신을 하기 시작하니 상태관리가 엄청나게 복잡해진다는 것을 알았습니다. 그래서 서버상태관리를 도입을 하면 보다 함수형 패러다임으로 선언적으로 비동기를 관리할 수 있다는 사실을 알게 되었습니다. @@ -72,13 +70,14 @@ TanstackQuery를 이용하여 코드를 개선하기 ```markdown 목표: -서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기 +서버상태관리 도구인 TanstackQuery를 이용하여 비동기코드를 선언적인 함수형 프로그래밍으로 작성하기 - TanstackQuery의 사용법에 대한 이해 - TanstackQuery를 이용한 비동기 코드 작성에 대한 이해 - 비동기 코드를 선언적인 함수형 프로그래밍으로 작성하는 방법에 대한 이해 체크포인트 + - [ ] 모든 API 호출이 TanStack Query의 useQuery와 useMutation으로 대체되었는가? - [ ] 쿼리 키가 적절히 설정되었는가? - [ ] fetch와 useState가 아닌 선언적인 함수형 프로그래밍이 적절히 적용되었는가? @@ -90,10 +89,9 @@ TanstackQuery를 이용하여 코드를 개선하기 - [ ] TanStack Query의 Devtools가 정상적으로 작동하는가? ``` - ## [6주차] 최종 과제 -FSD는 하나의 제안이지만, 여러분들은 FSD를 적용해보고 나면 조금 더 나은 구조를 만들 수 있다는 것을 알게 되었습니다. +FSD는 하나의 제안이지만, 여러분들은 FSD를 적용해보고 나면 조금 더 나은 구조를 만들 수 있다는 것을 알게 되었습니다. 그래서 여러분들은 FSD를 적용해보고 나서 다음과 같은 추가적인 목표를 세웠습니다. **조금 더 현대적이면서도 기능 중심의 폴더 구조를 만들 수 있지 않을까?** @@ -102,5 +100,6 @@ FSD는 하나의 제안이지만, 여러분들은 FSD를 적용해보고 나면 FSD가 아닌 자신만의 기능 중심의 폴더 구조를 만들어보세요. 꼭 기억할 점 -1. 자신만의 기능 중심의 폴더라고 했지만, 그 모습이 상당히 유니크하고 독창적이지는 않을 거에요. 아마 적절한 모법사례의 조합으로 수렴될 거에요. -2. 그리고 그게 잘하는 거에요. 좋은 코드는? 자신보돠 남들에게 모두에게 이해하기 쉬운 코드니까요. \ No newline at end of file + +1. 자신만의 기능 중심의 폴더라고 했지만, 그 모습이 상당히 유니크하고 독창적이지는 않을 거에요. 아마 적절한 모법사례의 조합으로 수렴될 거에요. +2. 그리고 그게 잘하는 거에요. 좋은 코드는? 자신보돠 남들에게 모두에게 이해하기 쉬운 코드니까요. diff --git a/docs/clean-code-analysis.md b/docs/clean-code-analysis.md new file mode 100644 index 000000000..4f2395b6f --- /dev/null +++ b/docs/clean-code-analysis.md @@ -0,0 +1,474 @@ +# 클린 코드 분석 보고서 + +## 분석 개요 + +현재 프로젝트를 Clean Code 원칙과 프론트엔드 디자인 가이드라인을 기반으로 분석한 결과를 정리합니다. + +## 주요 문제점 분석 + +### 1. 단일 책임 원칙(SRP) 위반 + +#### 문제점 + +**PostsManagerPage.tsx (815줄)** - 거대한 컴포넌트 + +- 18개의 useState로 과도한 상태 관리 +- 게시물, 댓글, 사용자, UI 상태를 모두 한 곳에서 처리 +- 함수들이 20줄을 초과하여 단일 책임 원칙 위반 + +```typescript +// 현재 문제 상황 (예시) +const PostsManager = () => { + // 18개의 useState - 너무 많은 책임 + const [posts, setPosts] = useState([]); + const [total, setTotal] = useState(0); + const [skip, setSkip] = useState(parseInt(queryParams.get('skip') || '0')); + const [limit, setLimit] = useState( + parseInt(queryParams.get('limit') || '10') + ); + // ... 14개 더 + + // 92줄의 fetchPosts 함수 - SRP 위반 + const fetchPosts = () => { + setLoading(true); + let postsData; + let usersData; + // 복잡한 로직... + }; +}; +``` + +#### 개선 방향 + +- 각 도메인별 컴포넌트 분리 (Posts, Comments, Users) +- 훅을 통한 로직 분리 (usePostsManager, useCommentsManager) +- 함수당 20줄 이하로 제한 + +### 2. 네이밍 규칙 위반 + +#### 문제점 분석 + +현재 코드의 네이밍이 일관성 없고 예측 가능하지 않음: + +```typescript +// ❌ 문제가 있는 네이밍 +const openPostDetail = post => {}; // 동사+명사 혼재 +const showAddDialog = false; // boolean은 is/has로 시작해야 함 +const newPost = {}; // 상태의 의미가 불명확 +const selectedPost = null; // null 초기값으로 타입 불명확 +``` + +#### Clean Code 네이밍 원칙 적용 + +```typescript +// ✅ 개선된 네이밍 +const displayPostDetail = post => {}; // 동작: display~() +const isAddDialogVisible = false; // 상태: is~ +const draftPost = {}; // 명확한 의미 +const currentSelectedPost = null; // 맥락 추가 +``` + +### 3. 타입 안전성 부족 + +#### 문제점 + +```typescript +// ❌ 타입 정의 부족 +const [posts, setPosts] = useState([]); // any[] +const [selectedPost, setSelectedPost] = useState(null); // any | null +const [comments, setComments] = useState({}); // any + +// API 응답 타입 미정의 +.then(response => response.json()) +.then(data => { // data: any + postsData = data; +}); +``` + +#### 개선 방향 + +```typescript +// ✅ 완전한 타입 정의 +interface Post { + id: number; + title: string; + body: string; + userId: number; + tags: string[]; + reactions: { + likes: number; + dislikes: number; + }; + author?: User; +} + +interface ApiResponse { + data: T; + total: number; + skip: number; + limit: number; +} + +const [posts, setPosts] = useState([]); +const [selectedPost, setSelectedPost] = useState(null); +``` + +### 4. 관심사 분리 부족 + +#### 문제점 - 코드 응집도(Cohesion) 문제 + +현재 하나의 컴포넌트에서 모든 관심사를 처리: + +```typescript +// ❌ 모든 관심사가 섞여 있음 +const PostsManager = () => { + // 1. URL 상태 관리 + const updateURL = () => {}; + + // 2. Posts CRUD + const fetchPosts = () => {}; + const addPost = () => {}; + const updatePost = () => {}; + const deletePost = () => {}; + + // 3. Comments CRUD + const fetchComments = () => {}; + const addComment = () => {}; + const updateComment = () => {}; + const deleteComment = () => {}; + + // 4. UI 상태 관리 + const openPostDetail = () => {}; + const openUserModal = () => {}; + + // 5. 검색/필터링 + const searchPosts = () => {}; + const fetchPostsByTag = () => {}; +}; +``` + +#### 개선 방향 - 도메인별 분리 + +```typescript +// ✅ 관심사별 분리 +// features/posts/ +const usePostsManager = () => {}; +const PostsList = () => {}; +const PostDetail = () => {}; + +// features/comments/ +const useCommentsManager = () => {}; +const CommentsList = () => {}; +const CommentForm = () => {}; + +// features/search/ +const useSearchManager = () => {}; +const SearchFilters = () => {}; + +// shared/hooks/ +const useUrlSync = () => {}; +``` + +### 5. 중복 코드 (DRY 원칙 위반) + +#### 문제점 + +CRUD 패턴이 반복되지만 추상화되지 않음: + +```typescript +// ❌ 중복되는 CRUD 패턴 +const addPost = async () => { + try { + const response = await fetch('/api/posts/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newPost), + }); + const data = await response.json(); + setPosts([data, ...posts]); + setShowAddDialog(false); + setNewPost({ title: '', body: '', userId: 1 }); + } catch (error) { + console.error('게시물 추가 오류:', error); + } +}; + +const addComment = async () => { + try { + const response = await fetch('/api/comments/add', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newComment), + }); + const data = await response.json(); + setComments(prev => ({ + ...prev, + [data.postId]: [...(prev[data.postId] || []), data], + })); + setShowAddCommentDialog(false); + setNewComment({ body: '', postId: null, userId: 1 }); + } catch (error) { + console.error('댓글 추가 오류:', error); + } +}; +``` + +#### 개선 방향 + +```typescript +// ✅ 공통 API 클라이언트 +const apiClient = { + async post(url: string, data: unknown): Promise { + const response = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + if (!response.ok) throw new Error(`API Error: ${response.status}`); + return response.json(); + }, +}; + +// ✅ 재사용 가능한 훅 +const useEntityManager = (endpoint: string) => { + const add = async (data: Partial) => { + return await apiClient.post(`${endpoint}/add`, data); + }; + // ... 다른 CRUD 메서드 +}; +``` + +### 6. 컴포넌트 결합도(Coupling) 문제 + +#### 문제점 - 거대한 렌더링 함수 + +```typescript +// ❌ 815줄의 거대한 컴포넌트 +return ( + + {/* 게시물 테이블 */} + {loading ? ( +
로딩 중...
+ ) : ( + + {/* 복잡한 테이블 로직 */} +
+ )} + + {/* 5개의 서로 다른 Dialog */} + ... + ... + ... + ... + ... + ... +
+); +``` + +#### 개선 방향 - 컴포넌트 조합 패턴 + +```typescript +// ✅ 작고 집중된 컴포넌트들의 조합 +const PostsManagerPage = () => { + return ( + + + + + + + ); +}; + +const PostsDialogManager = () => { + return ( + <> + + + + + + ); +}; +``` + +### 7. 매직 넘버 및 상수 관리 + +#### 문제점 + +```typescript +// ❌ 매직 넘버 남발 +const [limit, setLimit] = useState(parseInt(queryParams.get('limit') || '10')); +