create-web-0to1 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +68 -0
  2. package/internal/engine/create-feature-crud-script.ts +134 -0
  3. package/internal/engine/create-feature-crud.template.mjs +601 -0
  4. package/internal/engine/create-feature-script.ts +142 -0
  5. package/internal/engine/generator-engine.ts +546 -0
  6. package/internal/engine/standalone-feature-preset.ts +34 -0
  7. package/internal/meta/preset-plan.ts +41 -0
  8. package/internal/meta/runtime-copy-plan.ts +220 -0
  9. package/internal/meta/runtime-layout.ts +262 -0
  10. package/internal/meta/scaffold-manifest.ts +169 -0
  11. package/internal/meta/standalone-dependency-manifest.ts +75 -0
  12. package/package.json +45 -0
  13. package/scripts/create-app.mjs +1612 -0
  14. package/source/core/auth/auth-events.ts +13 -0
  15. package/source/core/error/app-error.ts +85 -0
  16. package/source/core/error/handle-app-error.client.ts +35 -0
  17. package/source/core/lib/dayjs.ts +25 -0
  18. package/source/core/query/query-client.ts +126 -0
  19. package/source/core/request/request-core.ts +210 -0
  20. package/source/core/routes/route-paths.ts +4 -0
  21. package/source/core/ui/button.tsx +24 -0
  22. package/source/core/ui/modal-store.ts +32 -0
  23. package/source/core/ui/text-input-field.tsx +36 -0
  24. package/source/core/utils/build-query-string.ts +30 -0
  25. package/source/core/utils/format/date.ts +41 -0
  26. package/source/core/utils/format/index.ts +3 -0
  27. package/source/core/utils/format/number.ts +13 -0
  28. package/source/core/utils/format/text.ts +15 -0
  29. package/source/core/utils/schema-utils.ts +27 -0
  30. package/source/wrappers/monorepo/core/internal.ts +21 -0
  31. package/source/wrappers/monorepo/core/src/index.ts +4 -0
  32. package/source/wrappers/monorepo/core-next/src/auth.client.ts +1 -0
  33. package/source/wrappers/monorepo/core-next/src/auth.server.ts +94 -0
  34. package/source/wrappers/monorepo/core-next/src/bootstrap.client.tsx +21 -0
  35. package/source/wrappers/monorepo/core-next/src/bootstrap.tsx +18 -0
  36. package/source/wrappers/monorepo/core-next/src/index.ts +1 -0
  37. package/source/wrappers/monorepo/core-react/src/app-providers.tsx +36 -0
  38. package/source/wrappers/monorepo/core-react/src/auth.ts +42 -0
  39. package/source/wrappers/monorepo/core-react/src/hydration.tsx +21 -0
  40. package/source/wrappers/monorepo/core-react/src/index.ts +7 -0
  41. package/source/wrappers/monorepo/core-react/src/provider.tsx +49 -0
  42. package/source/wrappers/monorepo/core-react/src/query-client.ts +48 -0
  43. package/source/wrappers/monorepo/core-react/src/query-error-handler.ts +62 -0
  44. package/source/wrappers/monorepo/core-react/src/query-keys.ts +22 -0
  45. package/source/wrappers/monorepo/request/core-fetch.ts +27 -0
  46. package/source/wrappers/monorepo/request/core-request.ts +93 -0
  47. package/source/wrappers/next/auth/auth-error-listener.tsx +34 -0
  48. package/source/wrappers/next/error/handle-app-error.server.ts +41 -0
  49. package/source/wrappers/next/query/hydration.tsx +20 -0
  50. package/source/wrappers/next/query/providers.tsx +35 -0
  51. package/source/wrappers/next/request/request.client.ts +24 -0
  52. package/source/wrappers/next/request/request.server.ts +64 -0
  53. package/source/wrappers/next/request/request.ts +52 -0
  54. package/source/wrappers/next/ui/global-modal.tsx +29 -0
  55. package/source/wrappers/react/auth/auth-error-listener.tsx +34 -0
  56. package/source/wrappers/react/query/providers.tsx +31 -0
  57. package/source/wrappers/react/request/request.client.ts +24 -0
  58. package/source/wrappers/react/request/request.ts +51 -0
  59. package/source/wrappers/react/ui/global-modal.tsx +27 -0
  60. package/templates/monorepo/.dockerignore +38 -0
  61. package/templates/monorepo/README.md +292 -0
  62. package/templates/monorepo/_gitignore +38 -0
  63. package/templates/monorepo/_npmrc +1 -0
  64. package/templates/monorepo/apps/project/Dockerfile +32 -0
  65. package/templates/monorepo/apps/project/eslint.config.mjs +4 -0
  66. package/templates/monorepo/apps/project/index.html +14 -0
  67. package/templates/monorepo/apps/project/index.ts +15 -0
  68. package/templates/monorepo/apps/project/package.json +21 -0
  69. package/templates/monorepo/apps/project/tsconfig.json +9 -0
  70. package/templates/monorepo/apps/project/vite.config.ts +6 -0
  71. package/templates/monorepo/apps/web/Dockerfile +43 -0
  72. package/templates/monorepo/apps/web/README.md +111 -0
  73. package/templates/monorepo/apps/web/_gitignore +36 -0
  74. package/templates/monorepo/apps/web/app/favicon.ico +0 -0
  75. package/templates/monorepo/apps/web/app/global-error.tsx +12 -0
  76. package/templates/monorepo/apps/web/app/globals.css +0 -0
  77. package/templates/monorepo/apps/web/app/layout.tsx +28 -0
  78. package/templates/monorepo/apps/web/app/page.tsx +7 -0
  79. package/templates/monorepo/apps/web/app/providers.tsx +25 -0
  80. package/templates/monorepo/apps/web/eslint.config.js +4 -0
  81. package/templates/monorepo/apps/web/next-env.d.ts +6 -0
  82. package/templates/monorepo/apps/web/next.config.js +4 -0
  83. package/templates/monorepo/apps/web/package.json +31 -0
  84. package/templates/monorepo/apps/web/public/file-text.svg +3 -0
  85. package/templates/monorepo/apps/web/public/globe.svg +10 -0
  86. package/templates/monorepo/apps/web/public/next.svg +1 -0
  87. package/templates/monorepo/apps/web/public/turborepo-dark.svg +19 -0
  88. package/templates/monorepo/apps/web/public/turborepo-light.svg +19 -0
  89. package/templates/monorepo/apps/web/public/vercel.svg +10 -0
  90. package/templates/monorepo/apps/web/public/window.svg +3 -0
  91. package/templates/monorepo/apps/web/tsconfig.json +20 -0
  92. package/templates/monorepo/package.json +24 -0
  93. package/templates/monorepo/packages/core/eslint.config.mjs +4 -0
  94. package/templates/monorepo/packages/core/package.json +32 -0
  95. package/templates/monorepo/packages/core/tsconfig.json +8 -0
  96. package/templates/monorepo/packages/core-next/eslint.config.mjs +13 -0
  97. package/templates/monorepo/packages/core-next/package.json +43 -0
  98. package/templates/monorepo/packages/core-next/tsconfig.json +8 -0
  99. package/templates/monorepo/packages/core-react/eslint.config.mjs +4 -0
  100. package/templates/monorepo/packages/core-react/package.json +34 -0
  101. package/templates/monorepo/packages/core-react/tsconfig.json +8 -0
  102. package/templates/monorepo/packages/eslint-config/README.md +3 -0
  103. package/templates/monorepo/packages/eslint-config/base.js +57 -0
  104. package/templates/monorepo/packages/eslint-config/next.js +22 -0
  105. package/templates/monorepo/packages/eslint-config/package.json +25 -0
  106. package/templates/monorepo/packages/eslint-config/react-internal.js +33 -0
  107. package/templates/monorepo/packages/typescript-config/base.json +19 -0
  108. package/templates/monorepo/packages/typescript-config/nextjs.json +12 -0
  109. package/templates/monorepo/packages/typescript-config/package.json +9 -0
  110. package/templates/monorepo/packages/typescript-config/react-library.json +7 -0
  111. package/templates/monorepo/packages/ui/eslint.config.mjs +4 -0
  112. package/templates/monorepo/packages/ui/package.json +26 -0
  113. package/templates/monorepo/packages/ui/src/button.tsx +20 -0
  114. package/templates/monorepo/packages/ui/src/card.tsx +27 -0
  115. package/templates/monorepo/packages/ui/src/code.tsx +11 -0
  116. package/templates/monorepo/packages/ui/tsconfig.json +8 -0
  117. package/templates/monorepo/pnpm-workspace.yaml +9 -0
  118. package/templates/monorepo/turbo/generators/config.js +1336 -0
  119. package/templates/monorepo/turbo/generators/templates/next-app/Dockerfile.tpl +30 -0
  120. package/templates/monorepo/turbo/generators/templates/next-app/README.md.tpl +118 -0
  121. package/templates/monorepo/turbo/generators/templates/next-app/app/global-error.tsx.tpl +12 -0
  122. package/templates/monorepo/turbo/generators/templates/next-app/app/globals.css.tpl +1 -0
  123. package/templates/monorepo/turbo/generators/templates/next-app/app/layout.tsx.tpl +29 -0
  124. package/templates/monorepo/turbo/generators/templates/next-app/app/page.tsx.tpl +7 -0
  125. package/templates/monorepo/turbo/generators/templates/next-app/app/providers.tsx.tpl +25 -0
  126. package/templates/monorepo/turbo/generators/templates/next-app/eslint.config.js.tpl +4 -0
  127. package/templates/monorepo/turbo/generators/templates/next-app/next.config.js.tpl +6 -0
  128. package/templates/monorepo/turbo/generators/templates/next-app/tsconfig.json.tpl +18 -0
  129. package/templates/monorepo/turbo/generators/templates/vite-app/Dockerfile.tpl +22 -0
  130. package/templates/monorepo/turbo/generators/templates/vite-app/README.plain.md.tpl +90 -0
  131. package/templates/monorepo/turbo/generators/templates/vite-app/README.react.md.tpl +107 -0
  132. package/templates/monorepo/turbo/generators/templates/vite-app/eslint.config.mjs.tpl +4 -0
  133. package/templates/monorepo/turbo/generators/templates/vite-app/index.html.tpl +12 -0
  134. package/templates/monorepo/turbo/generators/templates/vite-app/index.ts.tpl +22 -0
  135. package/templates/monorepo/turbo/generators/templates/vite-app/tsconfig.json.tpl +9 -0
  136. package/templates/monorepo/turbo/generators/templates/vite-app/vite.config.ts.tpl +6 -0
  137. package/templates/monorepo/turbo.json +28 -0
  138. package/templates/next/.env.example +2 -0
  139. package/templates/next/.prettierignore +9 -0
  140. package/templates/next/.prettierrc.json +9 -0
  141. package/templates/next/README.md +246 -0
  142. package/templates/next/_gitignore +44 -0
  143. package/templates/next/eslint.config.mjs +51 -0
  144. package/templates/next/next.config.ts +7 -0
  145. package/templates/next/package.json +24 -0
  146. package/templates/next/postcss.config.mjs +7 -0
  147. package/templates/next/scripts/create-feature-crud.mjs +5 -0
  148. package/templates/next/scripts/create-feature.mjs +5 -0
  149. package/templates/next/src/app/error.tsx +33 -0
  150. package/templates/next/src/app/globals.css +35 -0
  151. package/templates/next/src/app/layout.tsx +39 -0
  152. package/templates/next/src/app/login/page.tsx +17 -0
  153. package/templates/next/src/app/page.tsx +32 -0
  154. package/templates/next/src/app/providers.tsx +20 -0
  155. package/templates/next/tsconfig.json +34 -0
  156. package/templates/react/.env.example +1 -0
  157. package/templates/react/.prettierignore +10 -0
  158. package/templates/react/.prettierrc.json +9 -0
  159. package/templates/react/README.md +250 -0
  160. package/templates/react/_gitignore +31 -0
  161. package/templates/react/eslint.config.mjs +64 -0
  162. package/templates/react/package.json +19 -0
  163. package/templates/react/scripts/create-feature-crud.mjs +5 -0
  164. package/templates/react/scripts/create-feature.mjs +5 -0
  165. package/templates/react/src/app/app.tsx +15 -0
  166. package/templates/react/src/app/error-boundary.tsx +59 -0
  167. package/templates/react/src/app/frame.tsx +32 -0
  168. package/templates/react/src/app/globals.css +43 -0
  169. package/templates/react/src/app/not-found-page.tsx +23 -0
  170. package/templates/react/src/app/providers.tsx +16 -0
  171. package/templates/react/src/app/router.tsx +62 -0
  172. package/templates/react/src/main.tsx +12 -0
  173. package/templates/react/src/pages/index/page.tsx +36 -0
  174. package/templates/react/src/pages/login/page.tsx +18 -0
  175. package/templates/react/tsconfig.app.json +30 -0
  176. package/templates/react/tsconfig.json +4 -0
  177. package/templates/react/tsconfig.node.json +24 -0
  178. package/templates/react/vite.config.ts +14 -0
  179. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/00-/354/240/204/353/260/230/354/240/201/354/235/270-/355/217/264/353/215/224/352/265/254/354/241/260.md +150 -0
  180. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/01-/352/265/254/354/241/260/354/231/200-/353/235/274/354/232/260/355/214/205.md +186 -0
  181. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/02-/354/204/234/353/262/204/354/231/200-/355/201/264/353/235/274/354/235/264/354/226/270/355/212/270.md +86 -0
  182. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/03-/354/203/201/355/203/234/352/264/200/353/246/254.md +84 -0
  183. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/04-API/354/231/200-/353/215/260/354/235/264/355/204/260.md +199 -0
  184. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/05-/354/227/220/353/237/254/354/231/200-UI-/354/203/201/355/203/234.md +159 -0
  185. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/06-/355/217/274.md +116 -0
  186. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/07-/354/212/244/355/203/200/354/235/274/353/247/201/352/263/274-/354/240/221/352/267/274/354/204/261.md +73 -0
  187. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/08-/353/204/244/354/235/264/353/260/215-/354/204/244/354/240/225-/355/217/254/353/247/267/355/214/205.md +98 -0
  188. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/09-/353/235/274/354/232/260/355/212/270-/354/240/225/354/235/230.md +169 -0
  189. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/10-/354/273/244/353/260/213-/354/273/250/353/262/244/354/205/230.md +64 -0
  190. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/11-/352/270/260/355/203/200-/354/233/220/354/271/231.md +187 -0
  191. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/12-/354/213/244/353/254/264-/353/215/260/354/235/264/355/204/260-/355/214/250/355/204/264.md +302 -0
  192. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/13-/354/204/261/353/212/245-/354/233/220/354/271/231.md +175 -0
  193. package/templates/shared/docs//352/260/234/353/260/234/354/233/220/354/271/231/README.md +39 -0
@@ -0,0 +1,302 @@
1
+ # 실무 데이터 패턴
2
+
3
+ ## 검색 파라미터 원칙
4
+
5
+ ### 원칙
6
+
7
+ - 공유, 새로고침, 뒤로가기와 연결되어야 하는 상태는 검색 파라미터에 둡니다.
8
+ - `page`, `sort`, `search`, `filter`처럼 목록 조회 조건은 URL에 두는 쪽이 기본입니다.
9
+ - 입력 중 임시값이나 열림/닫힘 같은 UI 상태는 검색 파라미터보다 local state에 둡니다.
10
+ - 검색 조건이 바뀌면 `page`를 같이 초기화할지 먼저 정합니다.
11
+
12
+ ### Why?
13
+
14
+ - URL에 있는 상태는 링크 공유와 새로고침에 자연스럽게 이어집니다.
15
+ - 목록 조건이 local state에만 있으면 뒤로가기와 화면 복원이 어색해지기 쉽습니다.
16
+ - 반대로 모든 UI 상태를 URL에 올리면 주소가 불필요하게 복잡해집니다.
17
+
18
+ ### 예시 코드
19
+
20
+ ```tsx
21
+ // src/features/posts/components/post-filter.tsx
22
+ 'use client';
23
+
24
+ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
25
+
26
+ export function PostFilter() {
27
+ const pathname = usePathname();
28
+ const router = useRouter();
29
+ const searchParams = useSearchParams();
30
+
31
+ const keyword = searchParams.get('search') ?? '';
32
+
33
+ function updateSearch(value: string) {
34
+ const params = new URLSearchParams(searchParams.toString());
35
+
36
+ if (value) {
37
+ params.set('search', value);
38
+ } else {
39
+ params.delete('search');
40
+ }
41
+
42
+ params.delete('page');
43
+
44
+ const queryString = params.toString();
45
+ router.replace(queryString ? `${pathname}?${queryString}` : pathname);
46
+ }
47
+
48
+ return (
49
+ <input
50
+ value={keyword}
51
+ onChange={(event) => updateSearch(event.target.value)}
52
+ placeholder="검색어를 입력하세요"
53
+ />
54
+ );
55
+ }
56
+ ```
57
+
58
+ ### 흐름 예시: `searchParams -> query key -> 목록 조회`
59
+
60
+ ```ts
61
+ // src/features/posts/lib/post-list-params.ts
62
+ export type PostListParams = {
63
+ search: string;
64
+ page: number;
65
+ sort: 'latest' | 'popular';
66
+ };
67
+
68
+ export function parsePostListParams(searchParams: URLSearchParams): PostListParams {
69
+ const rawPage = Number(searchParams.get('page') ?? '1');
70
+ const rawSort = searchParams.get('sort');
71
+
72
+ return {
73
+ search: searchParams.get('search')?.trim() ?? '',
74
+ page: Number.isNaN(rawPage) || rawPage < 1 ? 1 : rawPage,
75
+ sort: rawSort === 'popular' ? 'popular' : 'latest',
76
+ };
77
+ }
78
+
79
+ export function buildPostListSearchParams(
80
+ current: URLSearchParams,
81
+ next: Partial<PostListParams>,
82
+ ) {
83
+ const params = new URLSearchParams(current.toString());
84
+
85
+ if (next.search !== undefined) {
86
+ if (next.search) {
87
+ params.set('search', next.search);
88
+ } else {
89
+ params.delete('search');
90
+ }
91
+
92
+ params.delete('page');
93
+ }
94
+
95
+ if (next.page !== undefined) {
96
+ params.set('page', String(next.page));
97
+ }
98
+
99
+ if (next.sort !== undefined) {
100
+ params.set('sort', next.sort);
101
+ }
102
+
103
+ return params;
104
+ }
105
+ ```
106
+
107
+ ```ts
108
+ // src/features/posts/hooks/query-keys.ts
109
+ import type { PostListParams } from '@/features/posts/lib/post-list-params';
110
+
111
+ export const postQueryKeys = {
112
+ all: ['posts'] as const,
113
+ list: (params: PostListParams) =>
114
+ [...postQueryKeys.all, 'list', params] as const,
115
+ };
116
+ ```
117
+
118
+ ```tsx
119
+ // src/features/posts/hooks/use-post-list-query.ts
120
+ 'use client';
121
+
122
+ import { useSearchParams } from 'next/navigation';
123
+ import { useQuery } from '@tanstack/react-query';
124
+
125
+ import { getPostListClient } from '@/features/posts/api/get-post-list.client';
126
+ import { postQueryKeys } from '@/features/posts/hooks/query-keys';
127
+ import { parsePostListParams } from '@/features/posts/lib/post-list-params';
128
+
129
+ export function usePostListQuery() {
130
+ const searchParams = useSearchParams();
131
+ const params = parsePostListParams(new URLSearchParams(searchParams.toString()));
132
+
133
+ return useQuery({
134
+ queryKey: postQueryKeys.list(params),
135
+ queryFn: () => getPostListClient(params),
136
+ });
137
+ }
138
+ ```
139
+
140
+ ```ts
141
+ // src/features/posts/lib/post-list-params.test.ts
142
+ import { describe, expect, it } from 'vitest';
143
+
144
+ import {
145
+ buildPostListSearchParams,
146
+ parsePostListParams,
147
+ } from '@/features/posts/lib/post-list-params';
148
+
149
+ describe('parsePostListParams', () => {
150
+ it('잘못된 page 값이면 1로 보정합니다', () => {
151
+ const result = parsePostListParams(
152
+ new URLSearchParams('search=react&page=-3&sort=popular'),
153
+ );
154
+
155
+ expect(result).toEqual({
156
+ search: 'react',
157
+ page: 1,
158
+ sort: 'popular',
159
+ });
160
+ });
161
+ });
162
+
163
+ describe('buildPostListSearchParams', () => {
164
+ it('search가 바뀌면 page를 제거합니다', () => {
165
+ const result = buildPostListSearchParams(
166
+ new URLSearchParams('search=next&page=3&sort=latest'),
167
+ { search: 'react' },
168
+ );
169
+
170
+ expect(result.toString()).toBe('search=react&sort=latest');
171
+ });
172
+ });
173
+ ```
174
+
175
+ ## 낙관적 업데이트 원칙
176
+
177
+ ### 원칙
178
+
179
+ - 결과를 쉽게 예측할 수 있고 롤백이 단순할 때만 낙관적 업데이트를 검토합니다.
180
+ - 좋아요 토글, 체크 상태 변경처럼 작은 상호작용에 더 잘 맞습니다.
181
+ - 복잡한 폼 저장이나 서버 응답이 중요한 작업은 성공 후 invalidate를 기본으로 둡니다.
182
+ - 낙관적 업데이트를 쓰면 실패 시 되돌리는 흐름까지 같이 설계합니다.
183
+
184
+ ### Why?
185
+
186
+ - 낙관적 업데이트는 체감 속도를 크게 올릴 수 있습니다.
187
+ - 하지만 실패 처리와 롤백이 애매하면 화면과 서버 상태가 쉽게 어긋납니다.
188
+ - 모든 mutation에 일괄 적용하기보다, 예측 가능한 작업에만 쓰는 편이 안전합니다.
189
+
190
+ ### 예시 코드
191
+
192
+ ```tsx
193
+ // src/features/posts/hooks/use-toggle-post-like-mutation.ts
194
+ 'use client';
195
+
196
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
197
+
198
+ import { togglePostLikeClient } from '@/features/posts/api/toggle-post-like.client';
199
+ import { postQueryKeys } from '@/features/posts/hooks/query-keys';
200
+ import type { Post } from '@/features/posts/types/post';
201
+
202
+ export function useTogglePostLikeMutation(postId: string) {
203
+ const queryClient = useQueryClient();
204
+
205
+ return useMutation({
206
+ mutationFn: () => togglePostLikeClient(postId),
207
+ onMutate: async () => {
208
+ await queryClient.cancelQueries({
209
+ queryKey: postQueryKeys.detail(postId),
210
+ });
211
+
212
+ const previousPost = queryClient.getQueryData<Post>(postQueryKeys.detail(postId));
213
+
214
+ if (previousPost) {
215
+ queryClient.setQueryData<Post>(postQueryKeys.detail(postId), {
216
+ ...previousPost,
217
+ liked: !previousPost.liked,
218
+ });
219
+ }
220
+
221
+ return { previousPost };
222
+ },
223
+ onError: (_error, _variables, context) => {
224
+ if (context?.previousPost) {
225
+ queryClient.setQueryData(postQueryKeys.detail(postId), context.previousPost);
226
+ }
227
+ },
228
+ onSettled: () => {
229
+ queryClient.invalidateQueries({
230
+ queryKey: postQueryKeys.detail(postId),
231
+ });
232
+ },
233
+ });
234
+ }
235
+ ```
236
+
237
+ ## DTO / 도메인 타입 분리 원칙
238
+
239
+ ### 원칙
240
+
241
+ - 서버 응답 형식은 DTO로 두고, 화면에서 쓰는 타입은 도메인 타입으로 분리할 수 있습니다.
242
+ - 응답 필드명이 UI에서 바로 쓰기 불편하거나 서버 스키마 변경 가능성이 크면 분리를 우선 검토합니다.
243
+ - DTO를 그대로 컴포넌트 전반에 퍼뜨리기보다 feature API 계층에서 도메인 타입으로 매핑합니다.
244
+ - 단순한 프로젝트에서는 무조건 분리하지 말고, 필요할 때만 도입합니다.
245
+
246
+ ### Why?
247
+
248
+ - 서버 응답 형식은 종종 UI가 원하는 이름과 다릅니다.
249
+ - DTO와 도메인 타입을 나누면 API 변경 영향이 feature API 계층에 더 잘 머뭅니다.
250
+ - 반대로 항상 두 타입을 만드는 건 과할 수 있어서, 분리 기준을 같이 가져가는 편이 좋습니다.
251
+
252
+ ### 예시 코드
253
+
254
+ ```ts
255
+ // src/features/posts/types/post.ts
256
+ export type Post = {
257
+ id: string;
258
+ title: string;
259
+ authorName: string;
260
+ publishedAt: string | null;
261
+ };
262
+ ```
263
+
264
+ ```ts
265
+ // src/features/posts/api/get-post-list.client.ts
266
+ import { requestClient } from '@/shared/api/request.client';
267
+
268
+ import type { Post } from '@/features/posts/types/post';
269
+
270
+ type PostDto = {
271
+ id: string;
272
+ title: string;
273
+ author_name: string;
274
+ published_at: string | null;
275
+ };
276
+
277
+ type GetPostListResponseDto = {
278
+ items: PostDto[];
279
+ };
280
+
281
+ function toPost(dto: PostDto): Post {
282
+ return {
283
+ id: dto.id,
284
+ title: dto.title,
285
+ authorName: dto.author_name,
286
+ publishedAt: dto.published_at,
287
+ };
288
+ }
289
+
290
+ export async function getPostListClient() {
291
+ const response = await requestClient<GetPostListResponseDto>({
292
+ method: 'GET',
293
+ url: '/posts',
294
+ });
295
+
296
+ return response.items.map(toPost);
297
+ }
298
+ ```
299
+
300
+ ## 한 줄 요약
301
+
302
+ 검색 파라미터는 공유 가능한 상태에, 낙관적 업데이트는 예측 가능한 변경에, DTO 분리는 필요할 때만 도입하는 쪽이 실무적으로 안정적입니다.
@@ -0,0 +1,175 @@
1
+ # 성능 원칙
2
+
3
+ ## 코드 스플리팅 원칙
4
+
5
+ ### 원칙
6
+
7
+ - 먼저 라우트 단위로 나눕니다.
8
+ - 초기 화면에 꼭 필요하지 않은 무거운 컴포넌트만 추가로 분리합니다.
9
+ - 에디터, 차트, 지도, 큰 모달처럼 번들 비용이 큰 UI에 우선 적용합니다.
10
+ - 작은 공통 UI나 항상 같이 렌더되는 컴포넌트는 과하게 나누지 않습니다.
11
+
12
+ ### Why?
13
+
14
+ - 코드 스플리팅은 초기 번들을 줄이는 데 도움이 됩니다.
15
+ - 반대로 너무 잘게 나누면 로딩 경계와 관리 포인트가 늘어납니다.
16
+ - 그래서 모든 컴포넌트를 나누기보다, 늦게 필요한 무거운 기능에 먼저 적용하는 편이 실용적입니다.
17
+
18
+ ### 예시 코드
19
+
20
+ ```tsx
21
+ // src/app/router.tsx
22
+ import { Suspense, lazy } from 'react';
23
+ import { createBrowserRouter, RouterProvider } from 'react-router-dom';
24
+
25
+ const PostListPage = lazy(() => import('@/pages/post-list-page'));
26
+ const PostDetailPage = lazy(() => import('@/pages/post-detail-page'));
27
+
28
+ const router = createBrowserRouter([
29
+ {
30
+ path: '/posts',
31
+ element: <PostListPage />,
32
+ },
33
+ {
34
+ path: '/posts/:id',
35
+ element: <PostDetailPage />,
36
+ },
37
+ ]);
38
+
39
+ export function AppRouter() {
40
+ return (
41
+ <Suspense fallback={<div>페이지를 불러오는 중...</div>}>
42
+ <RouterProvider router={router} />
43
+ </Suspense>
44
+ );
45
+ }
46
+ ```
47
+
48
+ ```tsx
49
+ // src/features/posts/components/create-post-page.tsx
50
+ import { Suspense, lazy } from 'react';
51
+
52
+ const PostEditor = lazy(() => import('@/features/posts/components/post-editor'));
53
+
54
+ export function CreatePostPage() {
55
+ return (
56
+ <Suspense fallback={<div>에디터를 불러오는 중...</div>}>
57
+ <PostEditor />
58
+ </Suspense>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## 재렌더링 최적화 원칙
64
+
65
+ ### 원칙
66
+
67
+ - `memo`, `useMemo`, `useCallback`을 기본값처럼 쓰지 않습니다.
68
+ - 새로운 프로젝트에서는 React Compiler 도입을 적극 검토합니다.
69
+ - 기존 프로젝트는 React 버전, 빌드 환경, 팀 사용 패턴을 같이 보고 점진적으로 도입합니다.
70
+ - 실제로 느리거나, 계산 비용이 크거나, 리스트가 큰 경우에만 검토합니다.
71
+ - 최적화는 상태 위치, props 구조, 컴포넌트 분리로 먼저 해결하고, 그다음 memoization을 봅니다.
72
+ - 입력 중 UI가 무거우면 `useDeferredValue`나 `startTransition` 같은 React 기능을 검토합니다.
73
+
74
+ ### Why?
75
+
76
+ - 과한 memoization은 코드만 복잡하게 만들고 효과는 거의 없을 수 있습니다.
77
+ - React Compiler는 반복적인 수동 memoization 부담을 줄이는 데 도움이 될 수 있습니다.
78
+ - 많은 경우 문제는 훅 하나가 아니라 상태 범위와 컴포넌트 구조에 있습니다.
79
+ - 다만 도입 여부는 프로젝트 환경과 운영 비용까지 같이 보고 정하는 편이 안전합니다.
80
+ - React가 이미 충분히 빠른 부분까지 미리 최적화하면 유지보수 비용만 커집니다.
81
+
82
+ ### 예시 코드
83
+
84
+ ```tsx
85
+ // src/features/posts/components/post-search.tsx
86
+ import { useDeferredValue, useMemo, useState } from 'react';
87
+
88
+ type Post = {
89
+ id: string;
90
+ title: string;
91
+ };
92
+
93
+ export function PostSearch({
94
+ posts,
95
+ }: {
96
+ posts: Post[];
97
+ }) {
98
+ const [keyword, setKeyword] = useState('');
99
+ const deferredKeyword = useDeferredValue(keyword);
100
+
101
+ const filteredPosts = useMemo(() => {
102
+ return posts.filter((post) =>
103
+ post.title.toLowerCase().includes(deferredKeyword.toLowerCase()),
104
+ );
105
+ }, [posts, deferredKeyword]);
106
+
107
+ return (
108
+ <div>
109
+ <input
110
+ value={keyword}
111
+ onChange={(event) => setKeyword(event.target.value)}
112
+ placeholder="검색어를 입력하세요"
113
+ />
114
+
115
+ <ul>
116
+ {filteredPosts.map((post) => (
117
+ <li key={post.id}>{post.title}</li>
118
+ ))}
119
+ </ul>
120
+ </div>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ## 큰 리스트 렌더링 원칙
126
+
127
+ ### 원칙
128
+
129
+ - 데이터가 많을수록 먼저 페이지네이션이나 무한 스크롤을 검토합니다.
130
+ - 한 번에 많은 행을 렌더해야 하면 가상화를 검토합니다.
131
+ - 리스트 아이템은 가능한 한 가볍게 유지합니다.
132
+ - 필터, 정렬, 선택 상태가 많아질수록 어떤 연산이 매 렌더마다 반복되는지 같이 봅니다.
133
+
134
+ ### Why?
135
+
136
+ - 리스트 성능 문제는 보통 한두 개 컴포넌트보다, 렌더되는 개수와 반복 연산에서 크게 발생합니다.
137
+ - 보이는 것만 렌더하면 DOM 수와 렌더 비용을 크게 줄일 수 있습니다.
138
+ - 리스트 아이템이 무거우면 가상화를 해도 체감 성능이 충분히 좋아지지 않을 수 있습니다.
139
+
140
+ ### 예시 코드
141
+
142
+ ```tsx
143
+ // src/features/posts/components/post-list.tsx
144
+ import { FixedSizeList as List } from 'react-window';
145
+
146
+ type Post = {
147
+ id: string;
148
+ title: string;
149
+ };
150
+
151
+ export function PostList({
152
+ posts,
153
+ }: {
154
+ posts: Post[];
155
+ }) {
156
+ return (
157
+ <List
158
+ height={560}
159
+ itemCount={posts.length}
160
+ itemSize={56}
161
+ width="100%"
162
+ >
163
+ {({ index, style }) => (
164
+ <div style={style}>
165
+ {posts[index].title}
166
+ </div>
167
+ )}
168
+ </List>
169
+ );
170
+ }
171
+ ```
172
+
173
+ ## 한 줄 요약
174
+
175
+ 성능 최적화는 모든 곳에 미리 적용하지 않고, 초기 번들 비용이 큰 영역, 재렌더링이 잦은 흐름, 큰 리스트처럼 실제로 비용이 큰 곳부터 다루는 편이 가장 실용적입니다.
@@ -0,0 +1,39 @@
1
+ # 프론트엔드 원칙, 패턴, 컨벤션 모음
2
+
3
+ 프론트엔드 개발에서 반복해서 마주치는 구조, 데이터, UI, 상태 관련 기준을 정리한 문서입니다.
4
+
5
+ > 한 곳에서 정하고, 책임에 따라 나누고, 전역 상태는 최소화합니다.
6
+
7
+ ## 문서 목록
8
+
9
+ - [00-전반적인-폴더구조.md](./00-전반적인-폴더구조.md): 전체 폴더 구조와 `feature`, `shared/components`, `shared/ui` 기준
10
+ - [01-구조와-라우팅.md](./01-구조와-라우팅.md): feature-first, layout, route, 컴포넌트 분리
11
+ - [02-서버와-클라이언트.md](./02-서버와-클라이언트.md): server/client 경계와 `use client` 기준
12
+ - [03-상태관리.md](./03-상태관리.md): URL, Query, Form, local/global state 기준
13
+ - [04-API와-데이터.md](./04-API와-데이터.md): request 계층, API layer, Query 규칙
14
+ - [05-에러와-UI-상태.md](./05-에러와-UI-상태.md): 전역/로컬 에러, 로딩, empty, 모달, 인증 에러
15
+ - [06-폼.md](./06-폼.md): `zod`, `react-hook-form`, 제출 UX
16
+ - [07-스타일링과-접근성.md](./07-스타일링과-접근성.md): 스타일 기준과 접근성
17
+ - [08-네이밍-설정-포맷팅.md](./08-네이밍-설정-포맷팅.md): 네이밍, 상수, 환경변수, 날짜/숫자 포맷팅
18
+ - [09-라우트-정의.md](./09-라우트-정의.md): route, menu, breadcrumb를 한 파일에서 관리하는 기준
19
+ - [10-커밋-컨벤션.md](./10-커밋-컨벤션.md): 커밋 메시지 타입과 작성 기준
20
+ - [11-기타-원칙.md](./11-기타-원칙.md): layout 반응형, layout state 범위, 영역별 provider 기준
21
+ - [12-실무-데이터-패턴.md](./12-실무-데이터-패턴.md): 검색 파라미터, 낙관적 업데이트, DTO / 도메인 타입 분리 기준
22
+ - [13-성능-원칙.md](./13-성능-원칙.md): 코드 스플리팅, 재렌더링, 큰 리스트 렌더링 기준
23
+
24
+ ## 읽는 순서
25
+
26
+ - 각 문서는 `원칙 -> Why? -> 예시 코드` 순서로 정리했습니다.
27
+ - 먼저 `00`, `01`, `02`, `03`을 읽으면 전체 구조를 잡기 좋습니다.
28
+ - 구현 기준이 필요할 때는 `04`, `05`, `06`, `12`, `13`을 같이 보면 연결이 잘 됩니다.
29
+ - 공통 규칙은 `07`, `08`, `09`, `10`, `11`, `13`에서 빠르게 확인할 수 있습니다.
30
+
31
+ ## 빠른 요약
32
+
33
+ - 구조는 기능 기준으로 나눕니다.
34
+ - `shared/ui`는 진짜 공통만 둡니다.
35
+ - `2~3개 feature` 공통은 `shared/components/[공유-맥락-이름]`도 사용할 수 있습니다.
36
+ - 상태는 성격에 맞는 계층에 둡니다.
37
+ - 공통 계층은 최대한 단순하게 유지합니다.
38
+ - low-level 계층이 UI를 직접 제어하지 않게 합니다.
39
+ - 이름만 봐도 역할이 보이게 짓습니다.