From 8d17562c40eeb95059e783ca0584c61bf886584c Mon Sep 17 00:00:00 2001 From: Rachel Date: Sun, 21 Sep 2025 16:26:39 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[8=EC=9E=A5]=20=EB=A6=AC=EC=95=A1=ED=8A=B8?= =?UTF-8?q?=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B4=80=EB=A6=AC=20-=20?= =?UTF-8?q?=EB=B0=95=EB=B3=B4=EB=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\353\260\225\353\263\264\353\235\274.md" | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 "8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" diff --git "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" new file mode 100644 index 0000000..63eefca --- /dev/null +++ "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" @@ -0,0 +1,98 @@ +# 8장 리액트 데이터 관리 + +> 이 장은 “비즈니스 로직 누수”, Prop Drilling, 그리고 이를 줄이기 위한 ACL(오류 방지 계층)·Context 전략을 다룬다. + +## 8.1 비즈니스 로직 누수 현상 + +🤔 비즈니스 로직이란? +- 어플리케이션을 운영하기 위한 정책, 계산 로직, 절차 등을 의미함 + +🐦‍🔥 비즈니스 로직 누수란? +- 비즈니스 로직과 관련 없는 컴포넌트나 어플리케이션 영역으로 흘러 들어가는 현상 +- 로직과 강한 결합을 나타내는 컴포넌트를 만들어냄 > 테스트, 유지보수를 어렵게 함 +- 비즈니스 로직이 어플리케이션 전반에 걸쳐 흩어져 있으면 코드 중복과 일관성이 부족해지는 현상이 발생한다. + +🫠 가장 흔한 예 : presentational 한 컴포넌트에서 데이터 변경하기 + + +## 8.2 ACL(오류 방지 계층, Anti-Corruption Layer) + +의도: 외부/불안정 소스(REST/3rd-party)에서 오는 형식/명세 차이를 “우리 앱 내부 모델”로 격리/정규화해 로직 누수를 막는다. (장 소개 근거) +FlipHTML5 + +예시 코드 — 외부 API → 내부 모델 변환 + +// external/api.ts (외부 응답 예시) +export type RawUser = { id: string; vip_flag?: 0|1; name?: string | null }; + +// acl/userMapper.ts (ACL: 외부 → 내부 정규화) +export type User = { id: string; isVip: boolean; name: string }; + +export function mapUser(raw: RawUser): User { + return { + id: raw.id, + isVip: raw.vip_flag === 1, + name: raw.name ?? '손님', + }; +} + +// app/repos/userRepo.ts (ACL을 경유) +import { mapUser, User } from '@/acl/userMapper'; +export async function fetchUser(id: string): Promise { + const res = await fetch(`/api/users/${id}`).then(r => r.json()); + return mapUser(res); // 내부로는 항상 User 보장 +} + + +테스트 포인트 + +ACL은 런타임 가드(zod/io-ts)와 함께 쓰면 효과↑. + +UI/서비스는 User만 알면 되므로 외부 필드 변경에도 안정적. + +8.3 Prop Drilling 문제 살펴보기 + +증상: 깊은 하위에서 필요한 값/행동을 위해 “중간 컴포넌트가 관심 없는 props”를 계속 전달. 유지보수/가독성/재사용성 저하. (항목 근거) +FlipHTML5 + +예시 코드 — Drilling이 심한 구조 + +// App -> PageLayout -> Toolbar -> UserMenu -> Avatar +export function App(){ return } +function PageLayout({ userName }: { userName:string }){ return } +function Toolbar({ userName }: { userName:string }){ return } +function UserMenu({ userName }: { userName:string }){ return } +function Avatar({ label }:{label:string}){ return {label} } + +8.4 Context API로 Prop Drilling 완화 + +해결책: 공통 상태/서비스를 Context로 노출해 중간 전달 제거. + +주의: Context 값 변경은 구독 트리 전체 재렌더 유발 가능—분할/메모·셀렉터 패턴 고려. (장 개요·문맥) +FlipHTML5 + +예시 코드 — Context + Selector 최소화 + +import React, { createContext, useContext, useMemo } from 'react'; + +type User = { name: string; isVip: boolean }; +const UserCtx = createContext(null); + +export function UserProvider({ children }: React.PropsWithChildren) { + // 실제로는 fetch + ACL 매핑을 통해 User 확보 + const value = useMemo(() => ({ name: '보라', isVip: true }), []); + return {children}; +} + +// 셀렉터 헬퍼 +export function useUser(selector: (u: User) => T): T { + const u = useContext(UserCtx); + if (!u) throw new Error('UserProvider 누락'); + return selector(u); +} + +// 소비자: 필요한 조각만 뽑기 +export function Avatar() { + const name = useUser(u => u.name); + return {name}; +} From bdaa9038c28c560f9d7af2c1e8022d94133eec41 Mon Sep 17 00:00:00 2001 From: Rachel Date: Wed, 24 Sep 2025 10:14:52 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[docs]=208=EC=9E=A5=20=EC=98=88=EC=8B=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\353\260\225\353\263\264\353\235\274.md" | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" index 63eefca..ec06c72 100644 --- "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" +++ "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" @@ -14,6 +14,43 @@ 🫠 가장 흔한 예 : presentational 한 컴포넌트에서 데이터 변경하기 +```tsx +import React from "react"; +import { User } from "../types"; + +export default function UserProfile({ user }: { user: User }) { + const [user, setUser] = useState(null); + + useEffect(() => { + async function fetchUser() { + + cosnt response = await fetch(`/api/users/${id}`); + const data = await response.json(); + + setUser({ + id: data.user_identifiaction, + name: data.user_full_name, + isPremium: data.is_premium_user, + subscription: data.subscription_details.level, + expire: data.subscription_details.expiry, + }); + } + + fetchUser(); + }, [id]): + + + if(!user) { + return
Loading...
+ } + + return ( +
+

{user.name}

+
+ ); +} +``` ## 8.2 ACL(오류 방지 계층, Anti-Corruption Layer) From c157e92d93a93763f23b417e13fbe25e68203973 Mon Sep 17 00:00:00 2001 From: Rachel Date: Wed, 24 Sep 2025 11:59:44 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[docs]=208=EC=9E=A5=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\353\260\225\353\263\264\353\235\274.md" | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" index ec06c72..b38404e 100644 --- "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" +++ "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" @@ -52,10 +52,15 @@ export default function UserProfile({ user }: { user: User }) { } ``` +🔍 그래서 저렇게 하면 뭐가 문제인데 ? +- 백엔드랑 주고 받는 데이터의 형식이 프론트에서 실제 노출하는 데이터 형식과 다를 수 있음. + - xml 데이터를 내부 타입으로 변환하기도 하고, GraphQL 엔드포인트를 통해 받은 데이터와 동일한 형태의 내부타입으로 변환하는 등.. + - 코드 영역에서 데이터 변환이 필요한데, 코드마다 이런 변환 작업이 들어가면, 유지보수가 힘들고 놓치는 부분 발생. +- 백엔드에서 `null`, `undefined` 같은 빈 값 데이터가 왔을 경우의 유저에게 노출되는 UI 처리를 개별적으로 해야 함. + ## 8.2 ACL(오류 방지 계층, Anti-Corruption Layer) -의도: 외부/불안정 소스(REST/3rd-party)에서 오는 형식/명세 차이를 “우리 앱 내부 모델”로 격리/정규화해 로직 누수를 막는다. (장 소개 근거) -FlipHTML5 +의도: 외부/불안정 소스(REST/3rd-party)에서 오는 형식/명세 차이를 “우리 앱 내부 모델”로 격리/정규화해 로직 누수를 막는다. 예시 코드 — 외부 API → 내부 모델 변환 From 449854ab5c0a6ccc9323a56cd8ea43a5fffa1833 Mon Sep 17 00:00:00 2001 From: Rachel Date: Wed, 24 Sep 2025 12:27:44 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[docs]=208=EC=9E=A5=20-=20ACL=20=EB=B6=80?= =?UTF-8?q?=EC=97=B0=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\353\260\225\353\263\264\353\235\274.md" | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" index b38404e..4474825 100644 --- "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" +++ "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" @@ -60,10 +60,14 @@ export default function UserProfile({ user }: { user: User }) { ## 8.2 ACL(오류 방지 계층, Anti-Corruption Layer) -의도: 외부/불안정 소스(REST/3rd-party)에서 오는 형식/명세 차이를 “우리 앱 내부 모델”로 격리/정규화해 로직 누수를 막는다. +- 외부/불안정 소스(REST/3rd-party)에서 오는 형식/명세 차이를 “우리 앱 내부 모델”로 격리/정규화해 로직 누수를 막는다. +- 캐시 처리, 오류 변화과 같은 여러 문제를 처리하는 전략 계층으로 활용 +- 여러 로직이 분산되는걸 막을 수 있음.(중앙 집중화) + 예시 코드 — 외부 API → 내부 모델 변환 +```ts // external/api.ts (외부 응답 예시) export type RawUser = { id: string; vip_flag?: 0|1; name?: string | null }; @@ -84,7 +88,7 @@ export async function fetchUser(id: string): Promise { const res = await fetch(`/api/users/${id}`).then(r => r.json()); return mapUser(res); // 내부로는 항상 User 보장 } - +``` 테스트 포인트 From 93bdac2644950a7f52cd68e0e91e9c12a7cba5c5 Mon Sep 17 00:00:00 2001 From: Rachel Date: Wed, 24 Sep 2025 13:09:58 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[docs]=208=EC=9E=A5=20ACL=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../\353\260\225\353\263\264\353\235\274.md" | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" index 4474825..9405b3c 100644 --- "a/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" +++ "b/8\354\236\245-\353\246\254\354\225\241\355\212\270 \353\215\260\354\235\264\355\204\260 \352\264\200\353\246\254/\353\260\225\353\263\264\353\235\274.md" @@ -90,16 +90,12 @@ export async function fetchUser(id: string): Promise { } ``` -테스트 포인트 - -ACL은 런타임 가드(zod/io-ts)와 함께 쓰면 효과↑. - -UI/서비스는 User만 알면 되므로 외부 필드 변경에도 안정적. +- ACL은 런타임 가드(zod/io-ts)와 함께 쓰면 효과적이다. +- UI/서비스는 User만 알면 되므로 외부 필드 변경에도 안정적이다. 8.3 Prop Drilling 문제 살펴보기 -증상: 깊은 하위에서 필요한 값/행동을 위해 “중간 컴포넌트가 관심 없는 props”를 계속 전달. 유지보수/가독성/재사용성 저하. (항목 근거) -FlipHTML5 +- 깊은 하위에서 필요한 값/행동을 위해 “중간 컴포넌트가 관심 없는 props”를 계속 전달. 유지보수/가독성/재사용성 저하. (항목 근거) 예시 코드 — Drilling이 심한 구조