@wooojin/forgen 0.4.8 → 0.4.9
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.
- package/.claude-plugin/plugin.json +1 -1
- package/assets/dev-guide/be/README.md +226 -0
- package/assets/dev-guide/be/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/be/principles/common.md +433 -0
- package/assets/dev-guide/be/principles/go.md +469 -0
- package/assets/dev-guide/be/principles/node.md +388 -0
- package/assets/dev-guide/be/skills/go/be-build/SKILL.md +262 -0
- package/assets/dev-guide/be/skills/go/be-perf/SKILL.md +308 -0
- package/assets/dev-guide/be/skills/go/be-review/SKILL.md +119 -0
- package/assets/dev-guide/be/skills/go/be-security/SKILL.md +362 -0
- package/assets/dev-guide/be/skills/node/be-build/SKILL.md +239 -0
- package/assets/dev-guide/be/skills/node/be-perf/SKILL.md +272 -0
- package/assets/dev-guide/be/skills/node/be-review/SKILL.md +118 -0
- package/assets/dev-guide/be/skills/node/be-security/SKILL.md +355 -0
- package/assets/dev-guide/be/sources/12factor/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/api-design/INDEX.md +56 -0
- package/assets/dev-guide/be/sources/ddia/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/go-runtime/INDEX.md +62 -0
- package/assets/dev-guide/be/sources/node-runtime/INDEX.md +60 -0
- package/assets/dev-guide/be/sources/otel/INDEX.md +53 -0
- package/assets/dev-guide/be/sources/owasp-api/INDEX.md +52 -0
- package/assets/dev-guide/be/sources/postgres/INDEX.md +55 -0
- package/assets/dev-guide/be/sources/sre-book/INDEX.md +48 -0
- package/assets/dev-guide/fe/README.md +197 -0
- package/assets/dev-guide/fe/adapters/build-agents-md.sh +63 -0
- package/assets/dev-guide/fe/adapters/refresh.sh +68 -0
- package/assets/dev-guide/fe/principles/common.md +160 -0
- package/assets/dev-guide/fe/principles/react.md +183 -0
- package/assets/dev-guide/fe/principles/vue.md +196 -0
- package/assets/dev-guide/fe/skills/react/fe-build/SKILL.md +139 -0
- package/assets/dev-guide/fe/skills/react/fe-perf/SKILL.md +179 -0
- package/assets/dev-guide/fe/skills/react/fe-review/SKILL.md +141 -0
- package/assets/dev-guide/fe/skills/vue/fe-build/SKILL.md +148 -0
- package/assets/dev-guide/fe/skills/vue/fe-perf/SKILL.md +163 -0
- package/assets/dev-guide/fe/skills/vue/fe-review/SKILL.md +136 -0
- package/assets/dev-guide/fe/sources/a11y-dx/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-memory.md +150 -0
- package/assets/dev-guide/fe/sources/a11y-dx/chrome-devtools-performance.md +99 -0
- package/assets/dev-guide/fe/sources/a11y-dx/lighthouse-audits.md +146 -0
- package/assets/dev-guide/fe/sources/a11y-dx/react-devtools-profiler.md +128 -0
- package/assets/dev-guide/fe/sources/a11y-dx/wcag22-new-criteria.md +174 -0
- package/assets/dev-guide/fe/sources/perf/01-core-web-vitals.md +58 -0
- package/assets/dev-guide/fe/sources/perf/02-inp.md +83 -0
- package/assets/dev-guide/fe/sources/perf/03-lcp-cls.md +130 -0
- package/assets/dev-guide/fe/sources/perf/04-speculation-rules.md +148 -0
- package/assets/dev-guide/fe/sources/perf/05-view-transitions.md +153 -0
- package/assets/dev-guide/fe/sources/perf/06-nextjs-caching.md +188 -0
- package/assets/dev-guide/fe/sources/perf/07-server-components.md +181 -0
- package/assets/dev-guide/fe/sources/perf/08-ppr.md +133 -0
- package/assets/dev-guide/fe/sources/perf/09-nextjs-image.md +200 -0
- package/assets/dev-guide/fe/sources/perf/10-optimize-lcp.md +201 -0
- package/assets/dev-guide/fe/sources/perf/INDEX.md +88 -0
- package/assets/dev-guide/fe/sources/react/INDEX.md +41 -0
- package/assets/dev-guide/fe/sources/react/keeping-components-pure.md +135 -0
- package/assets/dev-guide/fe/sources/react/no-effect-patterns.md +183 -0
- package/assets/dev-guide/fe/sources/react/react-compiler.md +182 -0
- package/assets/dev-guide/fe/sources/react/server-components.md +194 -0
- package/assets/dev-guide/fe/sources/react/server-functions.md +192 -0
- package/assets/dev-guide/fe/sources/react/suspense.md +218 -0
- package/assets/dev-guide/fe/sources/react/use-action-state.md +123 -0
- package/assets/dev-guide/fe/sources/react/use-form-status.md +158 -0
- package/assets/dev-guide/fe/sources/react/use-hook.md +153 -0
- package/assets/dev-guide/fe/sources/react/use-optimistic.md +194 -0
- package/assets/dev-guide/fe/sources/toss-ff/INDEX.md +58 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-code-directory.md +79 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-form-fields.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/cohesion-magic-number.md +47 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-item-edit-modal.md +124 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-bottom-sheet.md +57 -0
- package/assets/dev-guide/fe/sources/toss-ff/coupling-use-page-state.md +71 -0
- package/assets/dev-guide/fe/sources/toss-ff/overview-4-principles.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-hidden-logic.md +59 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-http.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/predictability-use-user.md +110 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-comparison-order.md +52 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-condition-name.md +64 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-login-start-page.md +183 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-magic-number.md +53 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-submit-button.md +73 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-ternary-operator.md +38 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-use-page-state.md +77 -0
- package/assets/dev-guide/fe/sources/toss-ff/readability-user-policy.md +98 -0
- package/assets/dev-guide/fe/sources/vue/INDEX.md +17 -0
- package/assets/dev-guide/fe/sources/vue/composition-api.md +251 -0
- package/assets/dev-guide/fe/sources/vue/nuxt-data-fetching.md +232 -0
- package/assets/dev-guide/fe/sources/vue/pinia-state-management.md +134 -0
- package/assets/dev-guide/fe/sources/vue/reactivity-pitfalls.md +261 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-a.md +117 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-b.md +231 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-c.md +86 -0
- package/assets/dev-guide/fe/sources/vue/style-guide-priority-d.md +72 -0
- package/dist/cli.js +42 -0
- package/dist/core/dashboard-cli.d.ts +12 -0
- package/dist/core/dashboard-cli.js +226 -0
- package/dist/core/dev-guide-injector.d.ts +26 -0
- package/dist/core/dev-guide-injector.js +137 -0
- package/dist/core/init.js +53 -0
- package/dist/core/lifecycle-classifier.d.ts +23 -0
- package/dist/core/lifecycle-classifier.js +104 -0
- package/dist/core/observability-backfill.d.ts +31 -0
- package/dist/core/observability-backfill.js +178 -0
- package/dist/core/observability-store.d.ts +58 -0
- package/dist/core/observability-store.js +195 -0
- package/dist/core/session-store.js +4 -0
- package/dist/core/spawn.d.ts +17 -0
- package/dist/core/spawn.js +179 -2
- package/dist/core/statusline-cli.js +34 -1
- package/dist/engine/compound-extractor.js +39 -0
- package/dist/engine/compound-loop.js +6 -0
- package/dist/engine/compound-retire.d.ts +20 -0
- package/dist/engine/compound-retire.js +85 -0
- package/dist/hooks/context-guard.js +25 -1
- package/dist/hooks/post-tool-use.js +48 -0
- package/dist/hooks/solution-injector.js +93 -0
- package/dist/host/install-claude.d.ts +6 -2
- package/dist/host/install-claude.js +74 -2
- package/dist/host/install-codex.d.ts +4 -0
- package/dist/host/install-codex.js +71 -0
- package/dist/host/install-orchestrator.js +1 -0
- package/package.json +6 -6
- package/plugin.json +1 -1
- package/scripts/postinstall.js +134 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 책임을 하나씩 관리하기
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/examples/use-page-state-coupling.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: coupling
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 책임을 하나씩 관리하기
|
|
9
|
+
|
|
10
|
+
쿼리 파라미터, 상태, API 호출과 같은 로직의 종류에 따라서 함수나 컴포넌트, Hook을 나누지 마세요. 한 번에 다루는 맥락의 종류가 많아져서 이해하기 힘들고 수정하기 어려운 코드가 돼요.
|
|
11
|
+
|
|
12
|
+
## 📝 코드 예시
|
|
13
|
+
|
|
14
|
+
다음 `usePageState()` Hook은 페이지 전체의 URL 쿼리 파라미터를 한 번에 관리해요.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
export function usePageState() {
|
|
18
|
+
const [query, setQuery] = useQueryParams({
|
|
19
|
+
cardId: NumberParam,
|
|
20
|
+
statementId: NumberParam,
|
|
21
|
+
dateFrom: DateParam,
|
|
22
|
+
dateTo: DateParam,
|
|
23
|
+
statusList: ArrayParam
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return useMemo(
|
|
27
|
+
() => ({
|
|
28
|
+
values: {
|
|
29
|
+
cardId: query.cardId ?? undefined,
|
|
30
|
+
statementId: query.statementId ?? undefined,
|
|
31
|
+
dateFrom: query.dateFrom == null ? defaultDateFrom : moment(query.dateFrom),
|
|
32
|
+
dateTo: query.dateTo == null ? defaultDateTo : moment(query.dateTo),
|
|
33
|
+
statusList: query.statusList as StatementStatusType[] | undefined
|
|
34
|
+
},
|
|
35
|
+
controls: {
|
|
36
|
+
setCardId: (cardId: number) => setQuery({ cardId }, "replaceIn"),
|
|
37
|
+
setStatementId: (statementId: number) => setQuery({ statementId }, "replaceIn"),
|
|
38
|
+
setDateFrom: (date?: Moment) => setQuery({ dateFrom: date?.toDate() }, "replaceIn"),
|
|
39
|
+
setDateTo: (date?: Moment) => setQuery({ dateTo: date?.toDate() }, "replaceIn"),
|
|
40
|
+
setStatusList: (statusList?: StatementStatusType[]) => setQuery({ statusList }, "replaceIn")
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
[query, setQuery]
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 👃 코드 냄새 맡아보기
|
|
49
|
+
|
|
50
|
+
### 결합도
|
|
51
|
+
|
|
52
|
+
이 Hook은 "이 페이지에 필요한 모든 쿼리 매개변수를 관리하는 것"이라는 광범위한 책임을 가지고 있어요. 이로 인해 페이지 내의 컴포넌트나 다른 훅들이 이 훅에 의존하게 될 수 있으며, 코드 수정을 할 때 영향 범위가 급격히 확장될 수 있어요.
|
|
53
|
+
|
|
54
|
+
## ✏️ 개선해보기
|
|
55
|
+
|
|
56
|
+
다음 코드와 같이 각각의 쿼리 파라미터별로 별도의 Hook을 작성할 수 있어요.
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
export function useCardIdQueryParam() {
|
|
60
|
+
const [cardId, _setCardId] = useQueryParam("cardId", NumberParam);
|
|
61
|
+
|
|
62
|
+
const setCardId = useCallback((cardId: number) => {
|
|
63
|
+
_setCardId({ cardId }, "replaceIn");
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
return [cardId ?? undefined, setCardId] as const;
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Hook이 담당하는 책임을 분리했기 때문에, 수정에 따른 영향이 갈 범위를 좁힐 수 있어요.
|
|
71
|
+
그래서 Hook을 수정했을 때 예상하지 못한 영향이 생기는 것을 막을 수 있어요.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 좋은 코드를 위한 4가지 기준
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: readability|predictability|cohesion|coupling
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 좋은 코드를 위한 4가지 기준
|
|
9
|
+
|
|
10
|
+
좋은 프론트엔드 코드는 **변경하기 쉬운** 코드예요.
|
|
11
|
+
새로운 요구사항을 구현하고자 할 때, 기존 코드를 수정하고 배포하기 수월한 코드가 좋은 코드죠.
|
|
12
|
+
코드가 변경하기 쉬운지는 4가지 기준으로 판단할 수 있어요.
|
|
13
|
+
|
|
14
|
+
## 1. 가독성
|
|
15
|
+
|
|
16
|
+
**가독성**(Readability)은 코드가 읽기 쉬운 정도를 말해요.
|
|
17
|
+
코드가 변경하기 쉬우려면 먼저 코드가 어떤 동작을 하는지 이해할 수 있어야 해요.
|
|
18
|
+
|
|
19
|
+
읽기 좋은 코드는 읽는 사람이 한 번에 머릿속에서 고려하는 맥락이 적고, 위에서 아래로 자연스럽게 이어져요.
|
|
20
|
+
|
|
21
|
+
### 가독성을 높이는 전략
|
|
22
|
+
|
|
23
|
+
- **맥락 줄이기**
|
|
24
|
+
- 같이 실행되지 않는 코드 분리하기
|
|
25
|
+
- 구현 상세 추상화하기
|
|
26
|
+
- 로직 종류에 따라 합쳐진 함수 쪼개기
|
|
27
|
+
- **이름 붙이기**
|
|
28
|
+
- 복잡한 조건에 이름 붙이기
|
|
29
|
+
- 매직 넘버에 이름 붙이기
|
|
30
|
+
- **위에서 아래로 읽히게 하기**
|
|
31
|
+
- 시점 이동 줄이기
|
|
32
|
+
- 삼항 연산자 단순하게 하기
|
|
33
|
+
|
|
34
|
+
## 2. 예측 가능성
|
|
35
|
+
|
|
36
|
+
**예측 가능성**(Predictability)이란, 함께 협업하는 동료들이 함수나 컴포넌트의 동작을 얼마나 예측할 수 있는지를 말해요.
|
|
37
|
+
예측 가능성이 높은 코드는 일관적인 규칙을 따르고, 함수나 컴포넌트의 이름과 파라미터, 반환 값만 보고도 어떤 동작을 하는지 알 수 있어요.
|
|
38
|
+
|
|
39
|
+
### 예측 가능성을 높이는 전략
|
|
40
|
+
|
|
41
|
+
- 이름 겹치지 않게 관리하기
|
|
42
|
+
- 같은 종류의 함수는 반환 타입 통일하기
|
|
43
|
+
- 숨은 로직 드러내기
|
|
44
|
+
|
|
45
|
+
## 3. 응집도
|
|
46
|
+
|
|
47
|
+
**응집도**(Cohesion)란, 수정되어야 할 코드가 항상 같이 수정되는지를 말해요.
|
|
48
|
+
응집도가 높은 코드는 코드의 한 부분을 수정해도 의도치 않게 다른 부분에서 오류가 발생하지 않아요.
|
|
49
|
+
|
|
50
|
+
> 가독성과 응집도는 서로 상충할 수 있어요. 일반적으로 응집도를 높이기 위해서는 변수나 함수를 추상화하는 등 가독성을 떨어뜨리는 결정을 해야 해요. 함께 수정되지 않으면 오류가 발생할 수 있는 경우에는, 응집도를 우선해서 코드를 공통화, 추상화하세요. 위험성이 높지 않은 경우에는, 가독성을 우선하여 코드 중복을 허용하세요.
|
|
51
|
+
|
|
52
|
+
### 응집도를 높이는 전략
|
|
53
|
+
|
|
54
|
+
- 함께 수정되는 파일을 같은 디렉토리에 두기
|
|
55
|
+
- 매직 넘버 없애기
|
|
56
|
+
- 폼의 응집도 생각하기
|
|
57
|
+
|
|
58
|
+
## 4. 결합도
|
|
59
|
+
|
|
60
|
+
**결합도**(Coupling)란, 코드를 수정했을 때의 영향범위를 말해요.
|
|
61
|
+
코드를 수정했을 때 영향범위가 적어서, 변경에 따른 범위를 예측할 수 있는 코드가 수정하기 쉬운 코드예요.
|
|
62
|
+
|
|
63
|
+
### 결합도를 낮추는 전략
|
|
64
|
+
|
|
65
|
+
- 책임을 하나씩 관리하기
|
|
66
|
+
- 중복 코드 허용하기
|
|
67
|
+
- Props Drilling 지우기
|
|
68
|
+
|
|
69
|
+
## 코드 품질 여러 각도로 보기
|
|
70
|
+
|
|
71
|
+
아쉽게도 이 4가지 기준을 모두 한꺼번에 충족하기는 어려워요.
|
|
72
|
+
|
|
73
|
+
예를 들어서, 함수나 변수가 항상 같이 수정되기 위해서 공통화 및 추상화하면, 응집도가 높아지죠. 그렇지만 코드가 한 차례 추상화되기 때문에 가독성이 떨어져요.
|
|
74
|
+
|
|
75
|
+
중복 코드를 허용하면, 코드의 영향범위를 줄일 수 있어서, 결합도를 낮출 수 있어요. 그렇지만 한쪽을 수정했을 때 다른 한쪽을 실수로 수정하지 못할 수 있어서, 응집도가 떨어지죠.
|
|
76
|
+
|
|
77
|
+
프론트엔드 개발자는 현재 직면한 상황을 바탕으로, 깊이 있게 고민하면서, 장기적으로 코드가 수정하기 쉽게 하기 위해서 어떤 가치를 우선해야 하는지 고민해야 해요.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 숨은 로직 드러내기
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/examples/hidden-logic.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: predictability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 숨은 로직 드러내기
|
|
9
|
+
|
|
10
|
+
함수나 컴포넌트의 이름, 파라미터, 반환 값에 드러나지 않는 숨은 로직이 있다면, 함께 협업하는 동료들이 동작을 예측하는 데에 어려움을 겪을 수 있어요.
|
|
11
|
+
|
|
12
|
+
## 📝 코드 예시
|
|
13
|
+
|
|
14
|
+
다음 코드는 사용자의 계좌 잔액을 조회할 때 사용할 수 있는 `fetchBalance` 함수예요. 함수를 호출할 때마다 암시적으로 `balance_fetched`라는 로깅이 이루어지고 있어요.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
async function fetchBalance(): Promise<number> {
|
|
18
|
+
const balance = await http.get<number>("...");
|
|
19
|
+
|
|
20
|
+
logging.log("balance_fetched");
|
|
21
|
+
|
|
22
|
+
return balance;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 👃 코드 냄새 맡아보기
|
|
27
|
+
|
|
28
|
+
### 예측 가능성
|
|
29
|
+
|
|
30
|
+
`fetchBalance` 함수의 이름과 반환 타입만을 가지고는 `balance_fetched` 라는 로깅이 이루어지는지 알 수 없어요. 그래서 로깅을 원하지 않는 곳에서도 로깅이 이루어질 수 있어요.
|
|
31
|
+
|
|
32
|
+
또, 로깅 로직에 오류가 발생했을 때 갑자기 계좌 잔액을 가져오는 로직이 망가질 수도 있죠.
|
|
33
|
+
|
|
34
|
+
## ✏️ 개선해보기
|
|
35
|
+
|
|
36
|
+
함수의 이름과 파라미터, 반환 타입으로 예측할 수 있는 로직만 구현 부분에 남기세요.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
async function fetchBalance(): Promise<number> {
|
|
40
|
+
const balance = await http.get<number>("...");
|
|
41
|
+
|
|
42
|
+
return balance;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
로깅을 하는 코드는 별도로 분리하세요.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<Button
|
|
50
|
+
onClick={async () => {
|
|
51
|
+
const balance = await fetchBalance();
|
|
52
|
+
logging.log("balance_fetched");
|
|
53
|
+
|
|
54
|
+
await syncBalance(balance);
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
계좌 잔액 갱신하기
|
|
58
|
+
</Button>
|
|
59
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 이름 겹치지 않게 관리하기
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/examples/http.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: predictability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 이름 겹치지 않게 관리하기
|
|
9
|
+
|
|
10
|
+
같은 이름을 가지는 함수나 변수는 동일한 동작을 해야 해요. 작은 동작 차이가 코드의 예측 가능성을 낮추고, 코드를 읽는 사람에게 혼란을 줄 수 있어요.
|
|
11
|
+
|
|
12
|
+
## 📝 코드 예시
|
|
13
|
+
|
|
14
|
+
어떤 프론트엔드 서비스에서 원래 사용하던 HTTP 라이브러리를 감싸서 새로운 형태로 HTTP 요청을 보내는 모듈을 만들었어요.
|
|
15
|
+
공교롭게 원래 HTTP 라이브러리와 새로 만든 HTTP 모듈의 이름은 `http`로 같아요.
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// http.ts
|
|
19
|
+
import { http as httpLibrary } from "@some-library/http";
|
|
20
|
+
|
|
21
|
+
export const http = {
|
|
22
|
+
async get(url: string) {
|
|
23
|
+
const token = await fetchToken();
|
|
24
|
+
|
|
25
|
+
return httpLibrary.get(url, {
|
|
26
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
// fetchUser.ts
|
|
34
|
+
import { http } from "./http";
|
|
35
|
+
|
|
36
|
+
export async function fetchUser() {
|
|
37
|
+
return http.get("...");
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 👃 코드 냄새 맡아보기
|
|
42
|
+
|
|
43
|
+
### 예측 가능성
|
|
44
|
+
|
|
45
|
+
이 코드는 기능적으로 문제가 없지만, 읽는 사람에게 혼란을 줄 수 있어요. `http.get`을 호출하는 개발자는 이 함수가 원래의 HTTP 라이브러리가 하는 것처럼 단순한 GET 요청을 보내는 것으로 예상하지만, 실제로는 토큰을 가져오는 추가 작업이 수행돼요.
|
|
46
|
+
|
|
47
|
+
오해로 인해서 기대 동작과 실제 동작의 차이가 생기고, 버그가 발생하거나, 디버깅 과정을 복잡하고 혼란스럽게 만들 수 있어요.
|
|
48
|
+
|
|
49
|
+
## ✏️ 개선해보기
|
|
50
|
+
|
|
51
|
+
서비스에서 만든 함수에는 라이브러리의 함수명과 구분되는 명확한 이름을 사용해서 함수의 동작을 예측 가능하게 만들 수 있어요.
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// httpService.ts
|
|
55
|
+
import { http as httpLibrary } from "@some-library/http";
|
|
56
|
+
|
|
57
|
+
export const httpService = {
|
|
58
|
+
async getWithAuth(url: string) {
|
|
59
|
+
const token = await fetchToken();
|
|
60
|
+
|
|
61
|
+
return httpLibrary.get(url, {
|
|
62
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// fetchUser.ts
|
|
70
|
+
import { httpService } from "./httpService";
|
|
71
|
+
|
|
72
|
+
export async function fetchUser() {
|
|
73
|
+
return await httpService.getWithAuth("...");
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
이렇게 해서 함수의 이름을 봤을 때 동작을 오해할 수 있는 가능성을 줄일 수 있어요.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 같은 종류의 함수는 반환 타입 통일하기
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/examples/use-user.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: predictability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 같은 종류의 함수는 반환 타입 통일하기
|
|
9
|
+
|
|
10
|
+
API 호출과 관련된 Hook들처럼 같은 종류의 함수나 Hook이 서로 다른 반환 타입을 가지면 코드의 일관성이 떨어져서, 같이 일하는 동료들이 코드를 읽는 데에 헷갈릴 수 있어요.
|
|
11
|
+
|
|
12
|
+
## 📝 코드 예시 1: useUser
|
|
13
|
+
|
|
14
|
+
다음 `useUser` 와 `useServerTime` Hook은 모두 API 호출과 관련된 Hook이에요.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
function useUser() {
|
|
18
|
+
const query = useQuery({
|
|
19
|
+
queryKey: ["user"],
|
|
20
|
+
queryFn: fetchUser
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return query; // Query 객체 반환
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function useServerTime() {
|
|
27
|
+
const query = useQuery({
|
|
28
|
+
queryKey: ["serverTime"],
|
|
29
|
+
queryFn: fetchServerTime
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return query.data; // 데이터만 반환
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 👃 코드 냄새 맡아보기
|
|
37
|
+
|
|
38
|
+
#### 예측 가능성
|
|
39
|
+
|
|
40
|
+
서버 API를 호출하는 Hook의 반환 타입이 서로 다르다면, 동료들은 이런 Hook을 쓸 때마다 반환 타입이 무엇인지 확인해야 해요.
|
|
41
|
+
|
|
42
|
+
같은 종류의 동작을 하는 코드가 일관적인 규칙에 따르고 있지 않으면 코드를 읽고 쓰는 데 헷갈려요.
|
|
43
|
+
|
|
44
|
+
### ✏️ 개선해보기
|
|
45
|
+
|
|
46
|
+
서버 API를 호출하는 Hook은 일관적으로 `Query` 객체를 반환하게 하면, 팀원들이 코드에 대한 예측 가능성을 높일 수 있어요.
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
function useUser() {
|
|
50
|
+
const query = useQuery({ queryKey: ["user"], queryFn: fetchUser });
|
|
51
|
+
return query;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function useServerTime() {
|
|
55
|
+
const query = useQuery({ queryKey: ["serverTime"], queryFn: fetchServerTime });
|
|
56
|
+
return query; // 이제 Query 객체 반환으로 통일
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 📝 코드 예시 2: checkIsValid
|
|
61
|
+
|
|
62
|
+
유효성 검사 함수도 동일한 패턴의 안티패턴이 나타나요.
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
/** 이름 검사: boolean 반환 */
|
|
66
|
+
function checkIsNameValid(name: string) {
|
|
67
|
+
return name.length > 0 && name.length < 20;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 나이 검사: { ok, reason } 객체 반환 */
|
|
71
|
+
function checkIsAgeValid(age: number) {
|
|
72
|
+
if (!Number.isInteger(age)) {
|
|
73
|
+
return { ok: false, reason: "나이는 정수여야 해요." };
|
|
74
|
+
}
|
|
75
|
+
if (age < 18) {
|
|
76
|
+
return { ok: false, reason: "나이는 18세 이상이어야 해요." };
|
|
77
|
+
}
|
|
78
|
+
if (age > 99) {
|
|
79
|
+
return { ok: false, reason: "나이는 99세 이하이어야 해요." };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 👃 코드 냄새 맡아보기
|
|
86
|
+
|
|
87
|
+
#### 예측 가능성
|
|
88
|
+
|
|
89
|
+
반환 값이 다르면 항상 객체인 `{ ok, ... }`는 truthy라서 `if (checkIsAgeValid(age))` 가 항상 실행되는 버그가 발생할 수 있어요.
|
|
90
|
+
|
|
91
|
+
### ✏️ 개선해보기
|
|
92
|
+
|
|
93
|
+
Discriminated Union 타입으로 통일:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
type ValidationCheckReturnType = { ok: true } | { ok: false; reason: string };
|
|
97
|
+
|
|
98
|
+
function checkIsNameValid(name: string): ValidationCheckReturnType {
|
|
99
|
+
if (name.length === 0) return { ok: false, reason: "이름은 빈 값일 수 없어요." };
|
|
100
|
+
if (name.length >= 20) return { ok: false, reason: "이름은 20자 이상 입력할 수 없어요." };
|
|
101
|
+
return { ok: true };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function checkIsAgeValid(age: number): ValidationCheckReturnType {
|
|
105
|
+
if (!Number.isInteger(age)) return { ok: false, reason: "나이는 정수여야 해요." };
|
|
106
|
+
if (age < 18) return { ok: false, reason: "나이는 18세 이상이어야 해요." };
|
|
107
|
+
if (age > 99) return { ok: false, reason: "나이는 99세 이하이어야 해요." };
|
|
108
|
+
return { ok: true };
|
|
109
|
+
}
|
|
110
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 왼쪽에서 오른쪽으로 읽히게 하기
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/examples/comparison-order.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: readability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 왼쪽에서 오른쪽으로 읽히게 하기
|
|
9
|
+
|
|
10
|
+
범위를 확인하는 조건문에서 부등호의 순서가 자연스럽지 않으면, 코드를 읽는 사람이 조건의 의도를 파악하는 데 시간이 더 걸려요.
|
|
11
|
+
|
|
12
|
+
## 📝 코드 예시
|
|
13
|
+
|
|
14
|
+
다음 코드들은 값이 특정 범위 안에 있는지 확인하는 조건문이에요.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
if (a >= b && a <= c) { ... }
|
|
18
|
+
|
|
19
|
+
if (score >= 80 && score <= 100) {
|
|
20
|
+
console.log("우수");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (price >= minPrice && price <= maxPrice) {
|
|
24
|
+
console.log("적정 가격");
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 👃 코드 냄새 맡아보기
|
|
29
|
+
|
|
30
|
+
### 가독성
|
|
31
|
+
|
|
32
|
+
이 코드들은 논리적으로는 올바르지만, 읽을 때 자연스럽지 않아요. `a >= b && a <= c`처럼 작성하면 중간값인 `a`를 두 번 확인해야 해서, 코드를 읽는 사람이 조건을 이해하기 위해 더 많은 인지적 부담을 느껴요.
|
|
33
|
+
|
|
34
|
+
수학에서 범위를 표현할 때처럼 `b ≤ a ≤ c` 형태로 왼쪽에서 오른쪽으로 자연스럽게 읽히면 더 직관적이에요.
|
|
35
|
+
|
|
36
|
+
## ✏️ 개선해보기
|
|
37
|
+
|
|
38
|
+
다음과 같이 범위의 시작점부터 끝점까지 왼쪽에서 오른쪽으로 읽히는 순서로 조건을 작성하면, 코드를 읽는 사람이 범위를 한눈에 파악할 수 있어요.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
if (b <= a && a <= c) { ... }
|
|
42
|
+
|
|
43
|
+
if (80 <= score && score <= 100) {
|
|
44
|
+
console.log("우수");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (minPrice <= price && price <= maxPrice) {
|
|
48
|
+
console.log("적정 가격");
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
이렇게 작성하면 `80 ≤ score ≤ 100`, `minPrice ≤ price ≤ maxPrice`처럼 수학의 부등식과 같은 형태로 읽혀서, 코드를 읽는 사람이 범위 조건을 직관적으로 이해할 수 있어요.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 복잡한 조건에 이름 붙이기
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/examples/condition-name.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: readability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 복잡한 조건에 이름 붙이기
|
|
9
|
+
|
|
10
|
+
복잡한 조건식이 특별한 이름 없이 사용되면, 조건이 뜻하는 바를 한눈에 파악하기 어려워요.
|
|
11
|
+
|
|
12
|
+
## 📝 코드 예시
|
|
13
|
+
|
|
14
|
+
다음 코드는 상품 중에서 카테고리와 가격 범위가 일치하는 상품만 필터링하는 로직이에요.
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
const result = products.filter((product) =>
|
|
18
|
+
product.categories.some(
|
|
19
|
+
(category) =>
|
|
20
|
+
category.id === targetCategory.id &&
|
|
21
|
+
product.prices.some((price) => price >= minPrice && price <= maxPrice)
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 👃 코드 냄새 맡아보기
|
|
27
|
+
|
|
28
|
+
### 가독성
|
|
29
|
+
|
|
30
|
+
이 코드에서는 익명 함수와 조건이 복잡하게 얽혀 있어요. `filter`와 `some`, `&&` 같은 로직이 여러 단계로 중첩되어 있어서 정확한 조건을 파악하기 어려워졌어요.
|
|
31
|
+
|
|
32
|
+
코드를 읽는 사람이 한 번에 고려해야 하는 맥락이 많아서, 가독성이 떨어져요.
|
|
33
|
+
|
|
34
|
+
## ✏️ 개선해보기
|
|
35
|
+
|
|
36
|
+
다음 코드와 같이 조건에 명시적인 이름을 붙이면, 코드를 읽는 사람이 한 번에 고려해야 할 맥락을 줄일 수 있어요.
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
const matchedProducts = products.filter((product) => {
|
|
40
|
+
return product.categories.some((category) => {
|
|
41
|
+
const isSameCategory = category.id === targetCategory.id;
|
|
42
|
+
const isPriceInRange = product.prices.some(
|
|
43
|
+
(price) => price >= minPrice && price <= maxPrice
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return isSameCategory && isPriceInRange;
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
명시적으로 같은 카테고리 안에 속해 있고, 가격 범위가 맞는 제품들로 필터링한다고 작성함으로써, 복잡한 조건식을 따라가지 않고도 코드의 의도를 명확히 드러낼 수 있어요.
|
|
52
|
+
|
|
53
|
+
## 🔍 더 알아보기: 조건식에 이름을 붙이는 기준
|
|
54
|
+
|
|
55
|
+
### 조건에 이름을 붙이는 것이 좋을 때
|
|
56
|
+
|
|
57
|
+
- **복잡한 로직을 다룰 때**: 조건문이나 함수에서 복잡한 로직이 여러 줄에 걸쳐 처리되면, 이름을 붙여 함수의 역할을 명확히 드러내는 것이 좋아요.
|
|
58
|
+
- **재사용성이 필요할 때**: 동일한 로직을 여러 곳에서 반복적으로 사용할 가능성이 있으면, 변수나 함수를 선언해 재사용할 수 있어요.
|
|
59
|
+
- **단위 테스트가 필요할 때**: 함수를 분리하면 독립적으로 단위 테스트를 작성할 수 있어요.
|
|
60
|
+
|
|
61
|
+
### 조건에 이름을 붙이지 않아도 괜찮을 때
|
|
62
|
+
|
|
63
|
+
- **로직이 간단할 때**: 로직이 매우 간단하면, 굳이 이름을 붙이지 않아도 돼요. 예를 들어, `arr.map(x => x * 2)`와 같은 코드는 이름을 붙이지 않아도 직관적이에요.
|
|
64
|
+
- **한 번만 사용될 때**: 특정 로직이 코드 내에서 한 번만 사용되며, 그 로직이 복잡하지 않으면 익명 함수에서 직접 로직을 처리하는 것이 더 직관적일 수 있어요.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: 구현 상세 추상화하기
|
|
3
|
+
source: https://frontend-fundamentals.com/code-quality/code/examples/login-start-page.html
|
|
4
|
+
fetched: 2026-05-18
|
|
5
|
+
principle: readability
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# 구현 상세 추상화하기
|
|
9
|
+
|
|
10
|
+
한 사람이 코드를 읽을 때 동시에 고려할 수 있는 총 맥락의 숫자는 제한되어 있다고 해요.
|
|
11
|
+
내 코드를 읽는 사람들이 코드를 쉽게 읽을 수 있도록 하기 위해서 불필요한 맥락을 추상화할 수 있어요.
|
|
12
|
+
|
|
13
|
+
## 📝 코드 예시 1: LoginStartPage
|
|
14
|
+
|
|
15
|
+
다음 `<LoginStartPage />` 컴포넌트는 사용자가 로그인되었는지 확인하고, 로그인이 된 경우 홈으로 이동시키는 로직을 가지고 있어요.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
function LoginStartPage() {
|
|
19
|
+
useCheckLogin({
|
|
20
|
+
onChecked: (status) => {
|
|
21
|
+
if (status === "LOGGED_IN") {
|
|
22
|
+
location.href = "/home";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/* ... 로그인 관련 로직 ... */
|
|
28
|
+
|
|
29
|
+
return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 👃 코드 냄새 맡아보기
|
|
34
|
+
|
|
35
|
+
#### 가독성
|
|
36
|
+
|
|
37
|
+
예시 코드에서는 로그인이 되었는지 확인하고, 사용자를 홈으로 이동시키는 로직이 추상화 없이 노출되어 있어요. 그래서 `useCheckLogin`, `onChecked`, `status`, `"LOGGED_IN"`과 같은 변수나 값을 모두 읽어야 무슨 역할을 하는 코드인지 알 수 있어요.
|
|
38
|
+
|
|
39
|
+
### ✏️ 개선해보기
|
|
40
|
+
|
|
41
|
+
사용자가 로그인되었는지 확인하고 이동하는 로직을 **HOC(Higher-Order Component)** 나 Wrapper 컴포넌트로 분리하여, 코드를 읽는 사람이 한 번에 알아야 하는 맥락을 줄여요.
|
|
42
|
+
|
|
43
|
+
#### 옵션 A: Wrapper 컴포넌트 사용하기
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
function App() {
|
|
47
|
+
return (
|
|
48
|
+
<AuthGuard>
|
|
49
|
+
<LoginStartPage />
|
|
50
|
+
</AuthGuard>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function AuthGuard({ children }) {
|
|
55
|
+
const status = useCheckLoginStatus();
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (status === "LOGGED_IN") {
|
|
59
|
+
location.href = "/home";
|
|
60
|
+
}
|
|
61
|
+
}, [status]);
|
|
62
|
+
|
|
63
|
+
return status !== "LOGGED_IN" ? children : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function LoginStartPage() {
|
|
67
|
+
/* ... 로그인 관련 로직 ... */
|
|
68
|
+
|
|
69
|
+
return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
#### 옵션 B: HOC(Higher-Order Component) 사용하기
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
function LoginStartPage() {
|
|
77
|
+
/* ... 로그인 관련 로직 ... */
|
|
78
|
+
|
|
79
|
+
return <>{/* ... 로그인 관련 컴포넌트 ... */}</>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default withAuthGuard(LoginStartPage);
|
|
83
|
+
|
|
84
|
+
function withAuthGuard(WrappedComponent) {
|
|
85
|
+
return function AuthGuard(props) {
|
|
86
|
+
const status = useCheckLoginStatus();
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (status === "LOGGED_IN") {
|
|
90
|
+
location.href = "/home";
|
|
91
|
+
}
|
|
92
|
+
}, [status]);
|
|
93
|
+
|
|
94
|
+
return status !== "LOGGED_IN" ? <WrappedComponent {...props} /> : null;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 📝 코드 예시 2: FriendInvitation
|
|
100
|
+
|
|
101
|
+
다음 `<FriendInvitation />` 컴포넌트는 클릭하면 사용자에게 동의를 받고 사용자에게 초대를 보내는 페이지 컴포넌트예요.
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
function FriendInvitation() {
|
|
105
|
+
const { data } = useQuery(/* 생략.. */);
|
|
106
|
+
|
|
107
|
+
const handleClick = async () => {
|
|
108
|
+
const canInvite = await overlay.openAsync(({ isOpen, close }) => (
|
|
109
|
+
<ConfirmDialog
|
|
110
|
+
title={`${data.name}님에게 공유해요`}
|
|
111
|
+
cancelButton={
|
|
112
|
+
<ConfirmDialog.CancelButton onClick={() => close(false)}>
|
|
113
|
+
닫기
|
|
114
|
+
</ConfirmDialog.CancelButton>
|
|
115
|
+
}
|
|
116
|
+
confirmButton={
|
|
117
|
+
<ConfirmDialog.ConfirmButton onClick={() => close(true)}>
|
|
118
|
+
확인
|
|
119
|
+
</ConfirmDialog.ConfirmButton>
|
|
120
|
+
}
|
|
121
|
+
/>
|
|
122
|
+
));
|
|
123
|
+
|
|
124
|
+
if (canInvite) {
|
|
125
|
+
await sendPush();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
<Button onClick={handleClick}>초대하기</Button>
|
|
132
|
+
{/* UI를 위한 JSX 마크업... */}
|
|
133
|
+
</>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### ✏️ 개선해보기
|
|
139
|
+
|
|
140
|
+
사용자에게 동의를 받는 로직과 버튼을 `<InviteButton />` 컴포넌트로 추상화했어요.
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
export function FriendInvitation() {
|
|
144
|
+
const { data } = useQuery(/* 생략.. */);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<InviteButton name={data.name} />
|
|
149
|
+
{/* UI를 위한 JSX 마크업 */}
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function InviteButton({ name }) {
|
|
155
|
+
return (
|
|
156
|
+
<Button
|
|
157
|
+
onClick={async () => {
|
|
158
|
+
const canInvite = await overlay.openAsync(({ isOpen, close }) => (
|
|
159
|
+
<ConfirmDialog
|
|
160
|
+
title={`${name}님에게 공유해요`}
|
|
161
|
+
cancelButton={
|
|
162
|
+
<ConfirmDialog.CancelButton onClick={() => close(false)}>
|
|
163
|
+
닫기
|
|
164
|
+
</ConfirmDialog.CancelButton>
|
|
165
|
+
}
|
|
166
|
+
confirmButton={
|
|
167
|
+
<ConfirmDialog.ConfirmButton onClick={() => close(true)}>
|
|
168
|
+
확인
|
|
169
|
+
</ConfirmDialog.ConfirmButton>
|
|
170
|
+
}
|
|
171
|
+
/>
|
|
172
|
+
));
|
|
173
|
+
|
|
174
|
+
if (canInvite) {
|
|
175
|
+
await sendPush();
|
|
176
|
+
}
|
|
177
|
+
}}
|
|
178
|
+
>
|
|
179
|
+
초대하기
|
|
180
|
+
</Button>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|