From fffd7adeb743ad2887acd24a96b76d38a89e75bf Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 13:11:00 +0900 Subject: [PATCH 01/16] =?UTF-8?q?docs(tech):=20=EA=B0=9D=EC=B2=B4=EC=A7=80?= =?UTF-8?q?=ED=96=A5=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=EC=9B=90=EC=B9=99=20=EB=8F=85=EC=84=9C=20=ED=9B=84=EA=B8=B0=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=EC=8B=9C=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\354\235\270\354\233\220\354\271\231.md" | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 "apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" new file mode 100644 index 00000000..4dd17823 --- /dev/null +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -0,0 +1,132 @@ +--- +slug: 객체지향-시스템-디자인-원칙 +title: 객체지향 시스템 디자인 원칙 (Simple Object Oriented Design) +date: 2025-09-14 +authors: [99mini] +tags: [독서, 객체지향] +--- + +[독서] 객체지향 시스템 디자인 원칙 (Simple Object Oriented Design) - 마우리시오 아니체 지음 + +[교보문고](https://product.kyobobook.co.kr/detail/S000216884277) + + + +## 서문 + +프론트엔드 개발자로 공부를 하면서 객체지향 프로그래밍을 적용하여 유자보수와 확장성을 높이는 방법을 고민하게 되었다. +이 책은 백엔드를 기반으로 하여 (예제 코드가 Java 기반의 pseudo code로 작성되어 있음) 객체지향 시스템 디자인 원칙을 설명하고 있지만, 프론트엔드 개발자에게도 많은 도움이 될 것이라 생각되어 읽게 되었다. + +## 책 소개 + +명확하고 간결한 객체지향 시스템 디자인 원칙을 제시하는 책이다. 6가지 원칙을 통해 유지보수성과 확장성을 높이는 방법을 예제와 함께 설명한다. + +- 코드를 작게 유지하는 방법 +- 객체의 일관성을 유지하는 방법 +- 의존성을 적절하게 관리하는 방법 +- 추상화를 이해하고 잘 디자인하는 방법 +- 인프라를 올바르게 처리하고 다루는 방법 +- 잘 모듈화된 디자인을 달성하는 방법 + +위 6가지 원칙의 세부내용 중 일부 원칙을 요약하며 프론트엔드 개발에 어떻게 적용할 수 있을지 고민해보았다. + +## 주요 내용 + +### 1. 코드를 작게 유지하라 + +#### 코드를 작은 단위로 나누지 말아야 할 때 + +> - 둘 이상의 퍼즐 조각이 독립적으로 존재할 수 없을 때. 강제로 분리하면 메서드 시그니처가 복잡해질 수 있다. +> - 퍼즐 코드 조각이 교체될 가능성이 낮을 때. +> - 해당 조각만 완전히 따로 떼어 테스트(단위 테스트)할 만한 가치가 없을 때. + +코드를 작은 단위로 나누는 것은 유지보수성과 확장성을 높이는 데 도움이 되지만, 지나치게 작은 단위로 나누면 오히려 복잡성을 증가시킬 수 있다. 따라서 코드의 응집도를 고려하여 적절한 크기로 유지하는 것이 중요하다. + +--- + +객체지향, 함수형 프로그래밍 등 다양한 패러다임을 실무에 적용하면서 처음부터 기능을 작은 단위로 나누거나 리펙토링 과정에서 기능을 작은 단위로 분리하는 경우가 많다. +그러나 작은 단위로 나누는 것이 항상 좋은 것은 아니다. 작은 단위로 나누면 코드의 응집도가 떨어지고, 오히려 복잡성이 증가할 수 있다. + +```jsx title="react에서 작은 단위로 나누는 경우" +// 작은 단위로 나누는 경우 +function UserProfile({ user }) { + return ( +
+ + + +
+ ); +} + +function UserAvatar({ avatar }) { + return User Avatar; +} + +function UserName({ name }) { + return

{name}

; +} + +function UserBio({ bio }) { + return

{bio}

; +} +``` + +위 예제는 UserProfile 컴포넌트를 작은 단위로 나눈 경우이다. 각 컴포넌트가 독립적으로 존재할 수 없고, 교체될 가능성이 낮으며, 단위 테스트할 만한 가치가 없다. 따라서 UserProfile 컴포넌트를 하나의 컴포넌트로 유지하는 것이 더 나을 수 있다. + +```jsx title="react에서 작은 단위로 나누지 않는 경우" +// 작은 단위로 나누지 않는 경우 +function UserProfile({ user }) { + return ( +
+ User Avatar +

{user.name}

+

{user.bio}

+
+ ); +} +``` + +그렇다면 언제 작은 단위로 나누는 것이 좋을까? + +- 둘 이상의 퍼즐 조각이 독립적으로 존재할 수 있을 때. + - UserAvatar 컴포넌트가 다른 곳에서도 재사용될 수 있다면 작은 단위로 나누는 것이 좋다. +- 퍼즐 코드 조각이 교체될 가능성이 높을 때. + - UserAvatar 컴포넌트가 다른 디자인으로 교체될 가능성이 있다면 작은 단위로 나누는 것이 좋다. +- 해당 조각만 완전히 따로 떼어 테스트(단위 테스트)할 만한 가치가 있을 때. + - UserAvatar 컴포넌트가 복잡한 로직을 가지고 있고, 단위 테스트할 필요가 있다면 작은 단위로 나누는 것이 좋다. + +```jsx title="react에서 작은 단위로 나누는 경우 (적절한 경우)" +// 작은 단위로 나누는 경우 (적절한 경우) +function UserProfile({ user }) { + return ( +
+ + + + +
+ ); +} +function UserAvatar({ avatar }) { + const avatarUrl = getAvatarUrl(avatar); // 복잡한 로직 + const handleAvatarClick = () => { + // 단위 테스트할 필요가 있는 로직 + console.log('Avatar clicked'); + }; + + return ( +
+ User Avatar + +
+ ); +} +``` + +### 2. 객체의 일관성을 유지하라 + +#### 항상 일관성을 유지하라 + +> - 클래스가 스스로 일관성을 책임지게 하라 +> - 전체 작업과 복잡한 일관성 검사를 캡슐화하라 From 505047b16ef6e1e0045e78b9fc7f08221680b725 Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 13:21:39 +0900 Subject: [PATCH 02/16] =?UTF-8?q?docs(blog):=20=EA=B0=9D=EC=B2=B4=20?= =?UTF-8?q?=EC=9D=BC=EA=B4=80=EC=84=B1=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\354\235\270\354\233\220\354\271\231.md" | 182 +++++++++++++++++- 1 file changed, 176 insertions(+), 6 deletions(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index 4dd17823..3a69fed6 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -30,11 +30,9 @@ tags: [독서, 객체지향] 위 6가지 원칙의 세부내용 중 일부 원칙을 요약하며 프론트엔드 개발에 어떻게 적용할 수 있을지 고민해보았다. -## 주요 내용 +## 1. 코드를 작게 유지하라 -### 1. 코드를 작게 유지하라 - -#### 코드를 작은 단위로 나누지 말아야 할 때 +### 코드를 작은 단위로 나누지 말아야 할 때 > - 둘 이상의 퍼즐 조각이 독립적으로 존재할 수 없을 때. 강제로 분리하면 메서드 시그니처가 복잡해질 수 있다. > - 퍼즐 코드 조각이 교체될 가능성이 낮을 때. @@ -124,9 +122,181 @@ function UserAvatar({ avatar }) { } ``` -### 2. 객체의 일관성을 유지하라 +## 2. 객체의 일관성을 유지하라 -#### 항상 일관성을 유지하라 +### 항상 일관성을 유지하라 > - 클래스가 스스로 일관성을 책임지게 하라 > - 전체 작업과 복잡한 일관성 검사를 캡슐화하라 + +객체의 상태가 일관성을 유지하도록 하는 것은 매우 중요하다. 객체가 일관성을 유지하지 않으면, 예기치 않은 동작이 발생할 수 있다. 따라서 객체는 스스로 일관성을 책임지도록 설계해야 한다. + +클리이언트는 절대 일관성 검사를 책임지지 말아야 하며, 기본적으로 클래스가 일관성을 관리해야 한다. 만약 클랙스에 들어가기에 검사가 너무 복잡하다면, 전체 작업을 서비스 클래스로 이동시키고 클라이언트가 원하는 동작을 위해 그 서비스 클래스를 사용해야 한다. [P.69] + +--- + +```jsx title="잘못된 예시 - 클라이언트가 일관성 검사를 담당" +// ❌ 잘못된 예시 - 클라이언트가 일관성을 책임짐 +function ShoppingCart() { + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + + const addItem = (item) => { + // 클라이언트가 일관성 검사를 담당 + if (item.price <= 0) { + throw new Error('가격은 0보다 커야 합니다'); + } + if (!item.name || item.name.trim() === '') { + throw new Error('상품명은 필수입니다'); + } + + setItems([...items, item]); + // 클라이언트가 total 계산도 담당 + setTotal(total + item.price); + }; + + return ( +
+ +
총 금액: {total}원
+
+ ); +} +``` + +```jsx title="올바른 예시 - 클래스가 일관성을 담당" +// ✅ 올바른 예시 - CartService가 일관성을 관리 +class CartService { + constructor() { + this.items = []; + } + + addItem(item) { + // 클래스 내부에서 일관성 검사 + this._validateItem(item); + this.items.push({ ...item }); + } + + removeItem(itemId) { + this.items = this.items.filter((item) => item.id !== itemId); + } + + getTotal() { + // 클래스가 총합 계산을 책임짐 + return this.items.reduce((sum, item) => sum + item.price, 0); + } + + getItems() { + return [...this.items]; // 불변성 유지 + } + + _validateItem(item) { + if (!item || typeof item !== 'object') { + throw new Error('유효하지 않은 상품입니다'); + } + if (!item.name || item.name.trim() === '') { + throw new Error('상품명은 필수입니다'); + } + if (typeof item.price !== 'number' || item.price <= 0) { + throw new Error('가격은 0보다 큰 숫자여야 합니다'); + } + if (!item.id) { + throw new Error('상품 ID는 필수입니다'); + } + } +} + +function ShoppingCart() { + const cartServiceRef = useRef(new CartService()); + const cartService = cartServiceRef.current; + + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + + const addItem = (item) => { + try { + cartService.addItem(item); + // 클라이언트는 단순히 상태만 업데이트 + setItems(cartService.getItems()); + setTotal(cartService.getTotal()); + } catch (error) { + alert(error.message); + } + }; + + return ( +
+ +
총 금액: {total}원
+
+ ); +} +``` + +```tsx title="올바른 예시 - 함수형 프로그래밍 스타일 일관성 담당" +// ✅ 올바른 예시 - 함수형 프로그래밍 스타일 +function validateItem(item) { + if (!item || typeof item !== 'object') { + throw new Error('유효하지 않은 상품입니다'); + } + if (!item.name || item.name.trim() === '') { + throw new Error('상품명은 필수입니다'); + } + if (typeof item.price !== 'number' || item.price <= 0) { + throw new Error('가격은 0보다 큰 숫자여야 합니다'); + } + if (!item.id) { + throw new Error('상품 ID는 필수입니다'); + } + return true; +} + +function addItem(items, item) { + validateItem(item); + return [...items, { ...item }]; // 불변성 유지 +} + +function ShoppingCart() { + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + + const handleAddItem = (item) => { + try { + // addItem 함수는 일관성 검사를 포함하는 순수 함수 + // 복사본을 반환함으로써 불변성 유지 + const newItems = addItem(items, item); + setItems(newItems); + // 총합 계산도 함수로 분리 + const newTotal = newItems.reduce((sum, item) => sum + item.price, 0); + setTotal(newTotal); + } catch (error) { + alert(error.message); + } + }; + + return ( +
+ +
총 금액: {total}원
+
+ ); +} +``` From c2d91a2461d0a9eb2c23e7c73c06a48562fa887b Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 13:32:40 +0900 Subject: [PATCH 03/16] =?UTF-8?q?docs(tech):=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EC=A3=BC=EC=9E=85,=20=ED=81=B4=EB=9E=98=EC=8A=A4=ED=98=95,?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=ED=98=95=20=EC=98=88=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\354\235\270\354\233\220\354\271\231.md" | 274 ++++++++++++++++++ 1 file changed, 274 insertions(+) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index 3a69fed6..f8acd196 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -300,3 +300,277 @@ function ShoppingCart() { ); } ``` + +## 3. 의존성을 적절하게 관리하라 + +### 의존성을 주입하라 (의존성 주입을 사용하라) + +> 의존성을 구성 요소에 주입하면 유연성과 테스트 가능성이 증가한다. 의존성을 주입할 수 있게 하면 구성 요소가 더 모듈화되고 독립적으로 쉽게 테스트할 수 있다. [P.111] + +실행 시점에 클래스에 서로 다른 구체적인 구현을 전달할 수 있으면 디자인의 유연성이 높아진다. [P.111] + +```tsx title="잘못된 예시 - 하드코딩된 의존성" +// ❌ 잘못된 예시 - 직접 의존성을 생성 +class UserService { + private httpClient: HttpClient; + private logger: Logger; + + constructor() { + // 하드코딩된 의존성 - 테스트하기 어렵고 유연성이 떨어짐 + this.httpClient = new HttpClient('https://api.example.com'); + this.logger = new ConsoleLogger(); + } + + async getUser(id: string): Promise { + this.logger.log(`Fetching user ${id}`); + return this.httpClient.get(`/users/${id}`); + } +} + +// React 컴포넌트에서 사용 +function UserProfile({ userId }: { userId: string }) { + const [user, setUser] = useState(null); + + useEffect(() => { + // 하드코딩된 서비스 인스턴스 + const userService = new UserService(); + userService.getUser(userId).then(setUser); + }, [userId]); + + return user ?
{user.name}
:
Loading...
; +} +``` + +```tsx title="올바른 예시 - 의존성 주입 사용" +// ✅ 올바른 예시 - 인터페이스 정의 +interface HttpClient { + get(url: string): Promise; + post(url: string, data: D): Promise; +} + +interface Logger { + log(message: string): void; + error(message: string): void; +} + +interface User { + id: string; + name: string; + email: string; +} + +// 구체적인 구현들 +class FetchHttpClient implements HttpClient { + constructor(private baseUrl: string) {} + + async get(url: string): Promise { + const response = await fetch(`${this.baseUrl}${url}`); + return response.json(); + } + + async post(url: string, data: D): Promise { + const response = await fetch(`${this.baseUrl}${url}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + return response.json(); + } +} + +class ConsoleLogger implements Logger { + log(message: string): void { + console.log(`[LOG] ${message}`); + } + + error(message: string): void { + console.error(`[ERROR] ${message}`); + } +} + +class SilentLogger implements Logger { + log(message: string): void {} + error(message: string): void {} +} + +// 의존성 주입을 받는 서비스 +class UserService { + constructor( + private httpClient: HttpClient, + private logger: Logger, + ) {} + + async getUser(id: string): Promise { + this.logger.log(`Fetching user ${id}`); + try { + return await this.httpClient.get(`/users/${id}`); + } catch (error) { + this.logger.error(`Failed to fetch user ${id}: ${error}`); + throw error; + } + } + + async createUser(userData: Omit): Promise { + this.logger.log(`Creating user ${userData.name}`); + return this.httpClient.post('/users', userData); + } +} + +// React Context를 사용한 의존성 주입 컨테이너 +interface ServiceContainer { + userService: UserService; + httpClient: HttpClient; + logger: Logger; +} + +const ServiceContext = React.createContext(null); + +// 의존성 주입 Provider +function ServiceProvider({ children }: { children: React.ReactNode }) { + const services = useMemo(() => { + const httpClient = new FetchHttpClient('https://api.example.com'); + const logger = process.env.NODE_ENV === 'production' ? new SilentLogger() : new ConsoleLogger(); + + return { + httpClient, + logger, + userService: new UserService(httpClient, logger), + }; + }, []); + + return {children}; +} + +// 서비스를 사용하는 커스텀 훅 +function useServices(): ServiceContainer { + const services = useContext(ServiceContext); + if (!services) { + throw new Error('useServices must be used within a ServiceProvider'); + } + return services; +} + +// React 컴포넌트에서 사용 +function UserProfile({ userId }: { userId: string }) { + const { userService } = useServices(); + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + userService + .getUser(userId) + .then(setUser) + .finally(() => setLoading(false)); + }, [userId, userService]); + + if (loading) return
Loading...
; + return user ? ( +
+ {user.name} - {user.email} +
+ ) : ( +
User not found
+ ); +} + +// 테스트용 Mock 구현 +class MockHttpClient implements HttpClient { + private mockData: Record = {}; + + setMockData(url: string, data: any) { + this.mockData[url] = data; + } + + async get(url: string): Promise { + const data = this.mockData[url]; + if (!data) throw new Error(`No mock data for ${url}`); + return Promise.resolve(data); + } + + async post(url: string, data: any): Promise { + return Promise.resolve(data as T); + } +} + +class TestLogger implements Logger { + public logs: string[] = []; + public errors: string[] = []; + + log(message: string): void { + this.logs.push(message); + } + + error(message: string): void { + this.errors.push(message); + } +} + +// 테스트 예시 +describe('UserService', () => { + test('should fetch user successfully', async () => { + const mockHttpClient = new MockHttpClient(); + const testLogger = new TestLogger(); + const userService = new UserService(mockHttpClient, testLogger); + + const mockUser: User = { id: '1', name: 'John Doe', email: 'john@example.com' }; + mockHttpClient.setMockData('/users/1', mockUser); + + const user = await userService.getUser('1'); + + expect(user).toEqual(mockUser); + expect(testLogger.logs).toContain('Fetching user 1'); + }); +}); +``` + +```tsx title="함수형 프로그래밍 스타일의 의존성 주입" +// ✅ 함수형 프로그래밍 스타일 의존성 주입 +type HttpClient = { + get: (url: string) => Promise; + post: (url: string, data: any) => Promise; +}; + +type Logger = { + log: (message: string) => void; + error: (message: string) => void; +}; + +type Dependencies = { + httpClient: HttpClient; + logger: Logger; +}; + +// 의존성을 받는 함수들 +const createUserOperations = (deps: Dependencies) => ({ + async getUser(id: string): Promise { + deps.logger.log(`Fetching user ${id}`); + try { + return await deps.httpClient.get(`/users/${id}`); + } catch (error) { + deps.logger.error(`Failed to fetch user ${id}: ${error}`); + throw error; + } + }, + + async createUser(userData: Omit): Promise { + deps.logger.log(`Creating user ${userData.name}`); + return deps.httpClient.post('/users', userData); + }, +}); + +// React 컴포넌트에서 사용 +function UserProfile({ userId }: { userId: string }) { + const { httpClient, logger } = useServices(); + const userOps = useMemo(() => createUserOperations({ httpClient, logger }), [httpClient, logger]); + + const [user, setUser] = useState(null); + + useEffect(() => { + userOps.getUser(userId).then(setUser); + }, [userId, userOps]); + + return user ?
{user.name}
:
Loading...
; +} +``` + +--- From 52fe72838aaf2affb70ad192af5c838522bf525a Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 13:52:57 +0900 Subject: [PATCH 04/16] =?UTF-8?q?docs(tech):=20=EC=B6=94=EC=83=81=ED=99=94?= =?UTF-8?q?=20=ED=99=9C=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\354\235\270\354\233\220\354\271\231.md" | 386 ++++++++++++++++++ 1 file changed, 386 insertions(+) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index f8acd196..4b0ab70e 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -573,4 +573,390 @@ function UserProfile({ userId }: { userId: string }) { } ``` +## 4. 추상화를 이해하고 잘 디자인하라 + +### 이쯤 되면 추상화를 고려해야 한다. + +> - 같은 클래스를 계속 반복적으로 수정하고 있는가? +> - 클래스가 계속 커지고 있는가? +> - 변화를 구현하기 위해 if 문을 계속 사용하는가? +> - 기존 비지니스 규칙을 시스템의 다른 부분에 결합시키는 과정이 어거지로 이어붙이는 것 같은가? +> +> [P.136] + +리액트 컴포넌트에서 반복적으로 사용되며 하나의 컴포넌트가 비대해지는 사례를 많이 만나볼 수 있다. +이 경우 추상화를 통해서 컴포넌트를 분리하거나 비지니스 로직을 커스텀 훅으로 분리하는 방법을 사용할 수 있다. +혹은 BaseComponent를 만들어 공통된 로직을 처리하고, HOC(고차 컴포넌트)를 활용하여 추상화 할 수도 있다. + +--- + +```tsx title="잘못된 예시 - if문을 통한 반복적인 분기 처리" +// ❌ 잘못된 예시 - 여러 컴포넌트에서 반복되는 권한 체크와 로딩 로직 +function UserProfile({ userId, userRole }: { userId: string; userRole: string }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // 권한 체크 + if (userRole !== 'admin' && userRole !== 'user') { + setError('권한이 없습니다'); + setLoading(false); + return; + } + + // 로딩 상태 처리 + setLoading(true); + fetchUser(userId) + .then(setUser) + .catch(() => setError('사용자를 불러올 수 없습니다')) + .finally(() => setLoading(false)); + }, [userId, userRole]); + + // 반복되는 조건부 렌더링 + if (loading) return
로딩 중...
; + if (error) return
{error}
; + if (!user) return
사용자를 찾을 수 없습니다
; + + return ( +
+

{user.name}

+

{user.email}

+
+ ); +} + +function ProjectList({ userRole }: { userRole: string }) { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // 동일한 권한 체크 로직 반복 + if (userRole !== 'admin' && userRole !== 'user') { + setError('권한이 없습니다'); + setLoading(false); + return; + } + + // 동일한 로딩 상태 처리 반복 + setLoading(true); + fetchProjects() + .then(setProjects) + .catch(() => setError('프로젝트를 불러올 수 없습니다')) + .finally(() => setLoading(false)); + }, [userRole]); + + // 동일한 조건부 렌더링 반복 + if (loading) return
로딩 중...
; + if (error) return
{error}
; + if (!projects.length) return
프로젝트가 없습니다
; + + return ( +
+ {projects.map((project) => ( +
{project.name}
+ ))} +
+ ); +} + +function Settings({ userRole }: { userRole: string }) { + const [settings, setSettings] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // 또 다시 반복되는 권한 체크 + if (userRole !== 'admin') { + setError('관리자 권한이 필요합니다'); + setLoading(false); + return; + } + + // 또 다시 반복되는 로딩 상태 처리 + setLoading(true); + fetchSettings() + .then(setSettings) + .catch(() => setError('설정을 불러올 수 없습니다')) + .finally(() => setLoading(false)); + }, [userRole]); + + // 또 다시 반복되는 조건부 렌더링 + if (loading) return
로딩 중...
; + if (error) return
{error}
; + if (!settings) return
설정을 찾을 수 없습니다
; + + return ( +
+

시스템 설정

+ {/* 설정 UI */} +
+ ); +} +``` + +```tsx title="올바른 예시 - HOC를 통한 추상화" +// ✅ 올바른 예시 - 공통 로직을 추상화한 HOC + +// 1. 권한 관리를 위한 HOC +interface WithAuthorizationProps { + userRole: string; +} + +type AuthorizedRoles = 'admin' | 'user' | 'guest'; + +function withAuthorization( + WrappedComponent: React.ComponentType, + allowedRoles: AuthorizedRoles[], +) { + return function AuthorizedComponent(props: T) { + const { userRole } = props; + + if (!allowedRoles.includes(userRole as AuthorizedRoles)) { + return
권한이 없습니다
; + } + + return ; + }; +} + +// 2. 비동기 데이터 로딩을 위한 HOC +interface WithAsyncDataProps { + loading?: boolean; + error?: string | null; + data?: any; +} + +function withAsyncData( + WrappedComponent: React.ComponentType, + dataFetcher: () => Promise, + emptyMessage: string = '데이터가 없습니다', +) { + return function AsyncDataComponent(props: Omit) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setLoading(true); + setError(null); + + dataFetcher() + .then(setData) + .catch((err) => setError(err.message || '데이터를 불러올 수 없습니다')) + .finally(() => setLoading(false)); + }, []); + + if (loading) return
로딩 중...
; + if (error) return
{error}
; + if (!data) return
{emptyMessage}
; + + return ; + }; +} + +// 3. 조합된 HOC를 사용한 컴포넌트들 +interface UserProfileProps extends WithAuthorizationProps { + userId: string; + data?: User; +} + +const BaseUserProfile: React.FC = ({ data: user }) => { + return ( +
+

{user!.name}

+

{user!.email}

+
+ ); +}; + +// HOC를 조합하여 최종 컴포넌트 생성 +const UserProfile = withAuthorization( + withAsyncData(BaseUserProfile, () => fetchUser('user-id'), '사용자를 찾을 수 없습니다'), + ['admin', 'user'], +); + +interface ProjectListProps extends WithAuthorizationProps { + data?: Project[]; +} + +const BaseProjectList: React.FC = ({ data: projects }) => { + return ( +
+ {projects!.map((project) => ( +
{project.name}
+ ))} +
+ ); +}; + +const ProjectList = withAuthorization(withAsyncData(BaseProjectList, fetchProjects, '프로젝트가 없습니다'), [ + 'admin', + 'user', +]); + +interface SettingsProps extends WithAuthorizationProps { + data?: Settings; +} + +const BaseSettings: React.FC = ({ data: settings }) => { + return ( +
+

시스템 설정

+ {/* 설정 UI */} +
+ ); +}; + +const Settings = withAuthorization( + withAsyncData(BaseSettings, fetchSettings, '설정을 찾을 수 없습니다'), + ['admin'], // 관리자만 접근 가능 +); +``` + +```tsx title="더 나은 예시 - 커스텀 훅과 함께 사용" +// ✅ 더 나은 예시 - 커스텀 훅으로 로직 분리 + +// 권한 체크 훅 +function useAuthorization(userRole: string, allowedRoles: AuthorizedRoles[]) { + const isAuthorized = allowedRoles.includes(userRole as AuthorizedRoles); + return { isAuthorized }; +} + +// 비동기 데이터 훅 +function useAsyncData(dataFetcher: () => Promise, deps: any[] = []) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setLoading(true); + setError(null); + + dataFetcher() + .then(setData) + .catch((err) => setError(err.message || '데이터를 불러올 수 없습니다')) + .finally(() => setLoading(false)); + }, deps); + + return { data, loading, error }; +} + +// 컴포넌트에서 훅 사용 +function UserProfile({ userId, userRole }: { userId: string; userRole: string }) { + const { isAuthorized } = useAuthorization(userRole, ['admin', 'user']); + const { data: user, loading, error } = useAsyncData(() => fetchUser(userId), [userId]); + + if (!isAuthorized) return
권한이 없습니다
; // 복잡한 컴포넌트로 교체할 수 있음 + if (loading) return
로딩 중...
; + if (error) return
{error}
; + if (!user) return
사용자를 찾을 수 없습니다
; + + return ( +
+

{user.name}

+

{user.email}

+
+ ); +} + +function ProjectList({ userRole }: { userRole: string }) { + const { isAuthorized } = useAuthorization(userRole, ['admin', 'user']); + const { data: projects, loading, error } = useAsyncData(fetchProjects); + + if (!isAuthorized) return
권한이 없습니다
; + if (loading) return
로딩 중...
; + if (error) return
{error}
; + if (!projects?.length) return
프로젝트가 없습니다
; + + return ( +
+ {projects.map((project) => ( +
{project.name}
+ ))} +
+ ); +} +``` + +```tsx title="고급 예시 - 제네릭과 타입 안전성을 활용한 HOC" +// ✅ 고급 예시 - 타입 안전한 HOC 패턴 + +// 권한 체크와 데이터 로딩을 결합한 고급 HOC +interface ProtectedAsyncComponentProps { + userRole: string; + data?: T; + loading?: boolean; + error?: string | null; +} + +function withProtectedAsyncData>( + WrappedComponent: React.ComponentType

, + options: { + dataFetcher: (props: Omit>) => Promise; + allowedRoles: AuthorizedRoles[]; + emptyMessage?: string; + errorMessage?: string; + }, +) { + return function ProtectedAsyncComponent( + props: Omit> & { userRole: string }, + ) { + const { userRole, ...restProps } = props; + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // 권한 체크 + const isAuthorized = options.allowedRoles.includes(userRole as AuthorizedRoles); + + useEffect(() => { + if (!isAuthorized) { + setLoading(false); + return; + } + + setLoading(true); + setError(null); + + options + .dataFetcher(restProps as any) + .then(setData) + .catch((err) => setError(options.errorMessage || err.message || '데이터를 불러올 수 없습니다')) + .finally(() => setLoading(false)); + }, [isAuthorized, JSON.stringify(restProps)]); + + if (!isAuthorized) { + return

권한이 없습니다
; + } + + if (loading) return
로딩 중...
; + if (error) return
{error}
; + if (!data) return
{options.emptyMessage || '데이터가 없습니다'}
; + + return ; + }; +} + +// 사용 예시 +interface UserProfileComponentProps extends ProtectedAsyncComponentProps { + userId: string; +} + +const UserProfileComponent: React.FC = ({ data: user }) => ( +
+

{user!.name}

+

{user!.email}

+
+); + +const UserProfile = withProtectedAsyncData(UserProfileComponent, { + dataFetcher: ({ userId }: { userId: string }) => fetchUser(userId), + allowedRoles: ['admin', 'user'], + emptyMessage: '사용자를 찾을 수 없습니다', + errorMessage: '사용자 정보를 불러오는데 실패했습니다', +}); +``` + --- From 86a5e8c8d6981c5be5738c70aae8df1ab0428ad7 Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 14:30:07 +0900 Subject: [PATCH 05/16] =?UTF-8?q?docs(tech):=20=EC=9D=B8=ED=94=84=EB=9D=BC?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\354\235\270\354\233\220\354\271\231.md" | 401 ++++++++++++++++++ 1 file changed, 401 insertions(+) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index 4b0ab70e..a3431025 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -959,4 +959,405 @@ const UserProfile = withProtectedAsyncData(UserProfileComponent, { }); ``` +## 5. 인프라를 올바르게 처리하고 다루라 + +> "인프라"를 포괄적인 용어로 사용 +> 인프라, 인프라 코드 같은 용어를 사용 +> 인프라: 데이터베이스(포스트그레스), 캐시 시스템(레디스), 외부 웹 서비스 +> 인프라 코드: 외부 시스템과 통합하기 위해 작성한 코드 + +### 도메인 코드와 인프라를 분리하라 + +> 인프라를 다루는 코드는 도메인 코드와 분리해야 한다. 이런 클래스는 가능한 얇게 작성해야 하며, 비지니스 로직을 포함해서는 안 된다. 이렇게 분리하면 디자인이 깔끔하고 진화하기 쉬우며, 테스트를 쉽게 할 수 있다. +> +> [P.141] + +비지니스 로직이 포함된 클래스 안에 인프라를 다루는 코드를 작성하지 마라. [P.141] + +```tsx title="잘못된 예시 - 도메인 로직과 인프라 코드가 섞임" +// ❌ 잘못된 예시 - 비즈니스 로직과 인프라 코드가 한 곳에 +class OrderService { + async createOrder(orderData: CreateOrderRequest): Promise { + // 비즈니스 로직과 검증 + if (!orderData.items || orderData.items.length === 0) { + throw new Error('주문 아이템이 없습니다'); + } + + // 할인 계산 비즈니스 로직 + let totalAmount = orderData.items.reduce((sum, item) => sum + item.price * item.quantity, 0); + if (totalAmount > 100000) { + totalAmount *= 0.9; // 10만원 이상 10% 할인 + } + + // 인프라 코드 - API 호출이 비즈니스 로직과 섞임 + const response = await fetch('/api/orders', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${localStorage.getItem('token')}`, + }, + body: JSON.stringify({ + items: orderData.items, + totalAmount, + customerId: orderData.customerId, + }), + }); + + if (!response.ok) { + throw new Error('주문 생성에 실패했습니다'); + } + + const order = await response.json(); + + // 인프라 코드 - 로컬 스토리지 직접 조작 + const recentOrders = JSON.parse(localStorage.getItem('recentOrders') || '[]'); + recentOrders.unshift(order); + if (recentOrders.length > 5) { + recentOrders.pop(); + } + localStorage.setItem('recentOrders', JSON.stringify(recentOrders)); + + // 인프라 코드 - 분석 이벤트 직접 전송 + gtag('event', 'purchase', { + transaction_id: order.id, + value: order.totalAmount, + currency: 'KRW', + }); + + return order; + } +} + +// React 컴포넌트에서도 인프라 로직이 섞임 +function OrderForm() { + const [cart, setCart] = useState([]); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (formData: OrderFormData) => { + setLoading(true); + + try { + // 비즈니스 로직 + const orderData = { + items: cart, + customerId: formData.customerId, + shippingAddress: formData.shippingAddress, + }; + + // 인프라 코드가 컴포넌트에 직접 포함 + const orderService = new OrderService(); + const order = await orderService.createOrder(orderData); + + // 성공 처리 + alert('주문이 완료되었습니다!'); + setCart([]); + } catch (error) { + alert('주문 처리 중 오류가 발생했습니다'); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 폼 UI */} +
+ ); +} +``` + +```tsx title="올바른 예시 - 도메인 로직과 인프라 분리" +// ✅ 올바른 예시 - 인프라 계층 분리 + +// 1. 인프라 계층 - API 통신만 담당 +interface OrderRepository { + create(order: CreateOrderRequest): Promise; + findById(id: string): Promise; +} + +class ApiOrderRepository implements OrderRepository { + constructor(private httpClient: HttpClient) {} + + async create(orderRequest: CreateOrderRequest): Promise { + return this.httpClient.post('/api/orders', orderRequest); + } + + async findById(id: string): Promise { + return this.httpClient.get(`/api/orders/${id}`); + } +} + +// 2. 인프라 계층 - 로컬 스토리지만 담당 +interface StorageService { + saveRecentOrder(order: Order): void; + getRecentOrders(): Order[]; +} + +class LocalStorageService implements StorageService { + saveRecentOrder(order: Order): void { + const recentOrders = this.getRecentOrders(); + recentOrders.unshift(order); + if (recentOrders.length > 5) { + recentOrders.pop(); + } + localStorage.setItem('recentOrders', JSON.stringify(recentOrders)); + } + + getRecentOrders(): Order[] { + return JSON.parse(localStorage.getItem('recentOrders') || '[]'); + } +} + +// 3. 인프라 계층 - 분석 이벤트만 담당 +interface AnalyticsService { + trackPurchase(order: Order): void; +} + +class GoogleAnalyticsService implements AnalyticsService { + trackPurchase(order: Order): void { + gtag('event', 'purchase', { + transaction_id: order.id, + value: order.totalAmount, + currency: 'KRW', + }); + } +} + +// 4. 도메인 계층 - 순수한 비즈니스 로직만 +class OrderDomain { + static validateOrder(orderData: CreateOrderRequest): void { + if (!orderData.items || orderData.items.length === 0) { + throw new Error('주문 아이템이 없습니다'); + } + + if (!orderData.customerId) { + throw new Error('고객 ID가 필요합니다'); + } + } + + static calculateDiscount(totalAmount: number): number { + if (totalAmount > 100000) { + return totalAmount * 0.1; // 10만원 이상 10% 할인 + } + return 0; + } + + static calculateTotalAmount(items: OrderItem[]): number { + return items.reduce((sum, item) => sum + item.price * item.quantity, 0); + } +} + +// 5. 애플리케이션 계층 - 도메인과 인프라를 조합 +class OrderApplicationService { + constructor( + private orderRepository: OrderRepository, + private storageService: StorageService, + private analyticsService: AnalyticsService + ) {} + + async createOrder(orderData: CreateOrderRequest): Promise { + // 도메인 로직 호출 + OrderDomain.validateOrder(orderData); + + const totalAmount = OrderDomain.calculateTotalAmount(orderData.items); + const discount = OrderDomain.calculateDiscount(totalAmount); + const finalAmount = totalAmount - discount; + + // 인프라를 통한 주문 생성 + const order = await this.orderRepository.create({ + ...orderData, + totalAmount: finalAmount, + discount, + }); + + // 인프라를 통한 부수 효과들 + this.storageService.saveRecentOrder(order); + this.analyticsService.trackPurchase(order); + + return order; + } +} + +// 6. React 컴포넌트 - UI 로직만 담당 +function OrderForm() { + const orderService = useOrderService(); // 의존성 주입 + const [cart, setCart] = useState([]); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (formData: OrderFormData) => { + setLoading(true); + + try { + const orderData: CreateOrderRequest = { + items: cart, + customerId: formData.customerId, + shippingAddress: formData.shippingAddress, + }; + + const order = await orderService.createOrder(orderData); + + // UI 관련 로직만 + alert('주문이 완료되었습니다!'); + setCart([]); + } catch (error) { + alert(error instanceof Error ? error.message : '주문 처리 중 오류가 발생했습니다'); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 폼 UI */} +
+ ); +} + +// 7. 의존성 주입 설정 +function useOrderService(): OrderApplicationService { + return useMemo(() => { + const httpClient = new FetchHttpClient(); + const orderRepository = new ApiOrderRepository(httpClient); + const storageService = new LocalStorageService(); + const analyticsService = new GoogleAnalyticsService(); + + return new OrderApplicationService( + orderRepository, + storageService, + analyticsService + ); + }, []); +} +``` + +```tsx title="더 나은 예시 - Custom Hook으로 관심사 분리" +// ✅ 더 나은 예시 - React 스타일의 관심사 분리 + +// 인프라 관련 커스텀 훅들 +function useOrderApi() { + const httpClient = useHttpClient(); + + const createOrder = useCallback(async (orderData: CreateOrderRequest): Promise => { + return httpClient.post('/api/orders', orderData); + }, [httpClient]); + + return { createOrder }; +} + +function useLocalStorage() { + const saveRecentOrder = useCallback((order: Order) => { + const recentOrders = JSON.parse(localStorage.getItem('recentOrders') || '[]'); + recentOrders.unshift(order); + if (recentOrders.length > 5) { + recentOrders.pop(); + } + localStorage.setItem('recentOrders', JSON.stringify(recentOrders)); + }, []); + + const getRecentOrders = useCallback((): Order[] => { + return JSON.parse(localStorage.getItem('recentOrders') || '[]'); + }, []); + + return { saveRecentOrder, getRecentOrders }; +} + +function useAnalytics() { + const trackPurchase = useCallback((order: Order) => { + gtag('event', 'purchase', { + transaction_id: order.id, + value: order.totalAmount, + currency: 'KRW', + }); + }, []); + + return { trackPurchase }; +} + +// 도메인 로직을 위한 커스텀 훅 +function useOrderDomain() { + const validateOrder = useCallback((orderData: CreateOrderRequest) => { + if (!orderData.items || orderData.items.length === 0) { + throw new Error('주문 아이템이 없습니다'); + } + if (!orderData.customerId) { + throw new Error('고객 ID가 필요합니다'); + } + }, []); + + const calculateTotalAmount = useCallback((items: OrderItem[]) => { + return items.reduce((sum, item) => sum + item.price * item.quantity, 0); + }, []); + + const calculateDiscount = useCallback((totalAmount: number) => { + return totalAmount > 100000 ? totalAmount * 0.1 : 0; + }, []); + + return { validateOrder, calculateTotalAmount, calculateDiscount }; +} + +// 종합 비즈니스 로직 훅 +function useOrderService() { + const orderApi = useOrderApi(); + const storage = useLocalStorage(); + const analytics = useAnalytics(); + const domain = useOrderDomain(); + + const createOrder = useCallback(async (orderData: CreateOrderRequest): Promise => { + // 도메인 로직 + domain.validateOrder(orderData); + + const totalAmount = domain.calculateTotalAmount(orderData.items); + const discount = domain.calculateDiscount(totalAmount); + const finalAmount = totalAmount - discount; + + // 인프라 호출 + const order = await orderApi.createOrder({ + ...orderData, + totalAmount: finalAmount, + discount, + }); + + // 부수 효과들 + storage.saveRecentOrder(order); + analytics.trackPurchase(order); + + return order; + }, [orderApi, storage, analytics, domain]); + + return { createOrder }; +} + +// React 컴포넌트는 UI만 담당 +function OrderForm() { + const { createOrder } = useOrderService(); + const [cart, setCart] = useState([]); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (formData: OrderFormData) => { + setLoading(true); + + try { + const order = await createOrder({ + items: cart, + customerId: formData.customerId, + shippingAddress: formData.shippingAddress, + }); + + alert('주문이 완료되었습니다!'); + setCart([]); + } catch (error) { + alert(error instanceof Error ? error.message : '주문 처리 중 오류가 발생했습니다'); + } finally { + setLoading(false); + } + }; + + return ( +
+ {/* 폼 UI */} +
+ ); +} +``` + --- From b02a5698223d649d520899009fbdba88deb95a77 Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 14:38:46 +0900 Subject: [PATCH 06/16] =?UTF-8?q?docs(tech):=20=EB=AA=A8=EB=93=88=ED=99=94?= =?UTF-8?q?=20=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\354\235\270\354\233\220\354\271\231.md" | 346 +++++++++++++++--- 1 file changed, 305 insertions(+), 41 deletions(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index a3431025..0e5da251 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -994,7 +994,7 @@ class OrderService { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem('token')}`, + Authorization: `Bearer ${localStorage.getItem('token')}`, }, body: JSON.stringify({ items: orderData.items, @@ -1058,11 +1058,7 @@ function OrderForm() { } }; - return ( -
- {/* 폼 UI */} -
- ); + return
{/* 폼 UI */}
; } ``` @@ -1152,7 +1148,7 @@ class OrderApplicationService { constructor( private orderRepository: OrderRepository, private storageService: StorageService, - private analyticsService: AnalyticsService + private analyticsService: AnalyticsService, ) {} async createOrder(orderData: CreateOrderRequest): Promise { @@ -1206,11 +1202,7 @@ function OrderForm() { } }; - return ( -
- {/* 폼 UI */} -
- ); + return
{/* 폼 UI */}
; } // 7. 의존성 주입 설정 @@ -1221,11 +1213,7 @@ function useOrderService(): OrderApplicationService { const storageService = new LocalStorageService(); const analyticsService = new GoogleAnalyticsService(); - return new OrderApplicationService( - orderRepository, - storageService, - analyticsService - ); + return new OrderApplicationService(orderRepository, storageService, analyticsService); }, []); } ``` @@ -1237,9 +1225,12 @@ function useOrderService(): OrderApplicationService { function useOrderApi() { const httpClient = useHttpClient(); - const createOrder = useCallback(async (orderData: CreateOrderRequest): Promise => { - return httpClient.post('/api/orders', orderData); - }, [httpClient]); + const createOrder = useCallback( + async (orderData: CreateOrderRequest): Promise => { + return httpClient.post('/api/orders', orderData); + }, + [httpClient], + ); return { createOrder }; } @@ -1302,27 +1293,30 @@ function useOrderService() { const analytics = useAnalytics(); const domain = useOrderDomain(); - const createOrder = useCallback(async (orderData: CreateOrderRequest): Promise => { - // 도메인 로직 - domain.validateOrder(orderData); + const createOrder = useCallback( + async (orderData: CreateOrderRequest): Promise => { + // 도메인 로직 + domain.validateOrder(orderData); - const totalAmount = domain.calculateTotalAmount(orderData.items); - const discount = domain.calculateDiscount(totalAmount); - const finalAmount = totalAmount - discount; + const totalAmount = domain.calculateTotalAmount(orderData.items); + const discount = domain.calculateDiscount(totalAmount); + const finalAmount = totalAmount - discount; - // 인프라 호출 - const order = await orderApi.createOrder({ - ...orderData, - totalAmount: finalAmount, - discount, - }); + // 인프라 호출 + const order = await orderApi.createOrder({ + ...orderData, + totalAmount: finalAmount, + discount, + }); - // 부수 효과들 - storage.saveRecentOrder(order); - analytics.trackPurchase(order); + // 부수 효과들 + storage.saveRecentOrder(order); + analytics.trackPurchase(order); - return order; - }, [orderApi, storage, analytics, domain]); + return order; + }, + [orderApi, storage, analytics, domain], + ); return { createOrder }; } @@ -1352,12 +1346,282 @@ function OrderForm() { } }; + return
{/* 폼 UI */}
; +} +``` + +## 6. 모듈화 달성하기 + +### 모듈을 분리하는 방법으로 이벤트를 고려하라 + +> 최근 이벤트 기반 아키텍처는 모듈과 서비스를 분리하는 놀라운 방법을 제공하여 인기을 얻고 있다. 이 아이디어는 모듈들이 후출을 통해 결합되는 대신, 무슨 일이 발생했는지 알라니는 이벤트를 발행하고, 관심 있는 모듈이 그 이벤트 스트림을 구독하는 것이다. +> +> [P.176] + +이벤트 기반 아키텍쳐를 프론트엔드에 적용 하는 방법은 여러가지가 있다. + +- 이벤트 버스를 만들어서 컴포넌트 간에 이벤트를 발행하고 구독 +- 상태 관리 라이브러리(Redux, Recoil, Zustand 등)의 구독 기능 활용 +- side effect(window event 등) 처리를 위한 pub/sub 패턴 활용 + +```tsx title="잘못된 예시 - 컴포넌트 간의 강한 결합" +// ❌ 잘못된 예시 - 컴포넌트 간의 강한 결합 +function ShoppingCart() { + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + + const addItem = (item: CartItem) => { + setItems((prev) => [...prev, item]); + setTotal((prev) => prev + item.price * item.quantity); + }; + return ( -
- {/* 폼 UI */} -
+
+ + +
); } + +function ProductList({ onAddToCart }: { onAddToCart: (item: CartItem) => void }) { + const products = useProducts(); + + return ( +
+ {products.map((product) => ( +
+

{product.name}

+ +
+ ))} +
+ ); +} + +function CartSummary({ items, total }: { items: CartItem[]; total: number }) { + return ( +
+

장바구니

+
    + {items.map((item, index) => ( +
  • + {item.name} - {item.quantity} x {item.price} +
  • + ))} +
+

총합: {total}

+
+ ); +} + +// ProductList 컴포넌트는 ShoppingCart에 강하게 결합되어 있음 ``` ---- +```tsx title="올바른 예시 - 이벤트 버스를 통한 느슨한 결합" +// ✅ 올바른 예시 - 이벤트 버스를 통한 느슨한 결합 +// 1. 간단한 이벤트 버스 구현 +type EventCallback = (data?: any) => void; +class EventBus { + private listeners: Record = {}; + + subscribe(event: string, callback: EventCallback) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(callback); + } + + unsubscribe(event: string, callback: EventCallback) { + if (!this.listeners[event]) return; + this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback); + } + + publish(event: string, data?: any) { + if (!this.listeners[event]) return; + this.listeners[event].forEach((callback) => callback(data)); + } +} + +const eventBus = new EventBus(); +// 2. 컴포넌트에서 이벤트 버스 사용 +function ShoppingCart() { + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + useEffect(() => { + const handleAddToCart = (item: CartItem) => { + setItems((prev) => [...prev, item]); + setTotal((prev) => prev + item.price * item.quantity); + }; + + eventBus.subscribe('addToCart', handleAddToCart); + return () => { + eventBus.unsubscribe('addToCart', handleAddToCart); + }; + }, []); + return ( +
+ + +
+ ); +} + +function ProductList() { + const products = useProducts(); + return ( +
+ {products.map((product) => ( +
+

{product.name}

+ +
+ ))} +
+ ); +} + +function CartSummary({ items, total }: { items: CartItem[]; total: number }) { + return ( +
+

장바구니

+
    + {items.map((item, index) => ( +
  • + {item.name} - {item.quantity} x {item.price} +
  • + ))} +
+

총합: {total}

+
+ ); +} +// ProductList 컴포넌트는 ShoppingCart에 느슨하게 결합됨 +``` + +```tsx title="더 나은 예시 - 상태 관리 라이브러리 활용" +// ✅ 더 나은 예시 - 상태 관리 라이브러리 활용 (예: Zustand) +import create from 'zustand'; + +// 1. Zustand를 이용한 글로벌 상태 관리 +interface CartState { + items: CartItem[]; + total: number; + addItem: (item: CartItem) => void; +} +const useCartStore = create((set) => ({ + items: [], + total: 0, + addItem: (item) => + set((state) => ({ + items: [...state.items, item], + total: state.total + item.price * item.quantity, + })), +})); +// 2. 컴포넌트에서 Zustand 상태 사용 +function ShoppingCart() { + const items = useCartStore((state) => state.items); + const total = useCartStore((state) => state.total); + return ( +
+ + +
+ ); +} + +function ProductList() { + const products = useProducts(); + const addItem = useCartStore((state) => state.addItem); + return ( +
+ {products.map((product) => ( +
+

{product.name}

+ +
+ ))} +
+ ); +} + +function CartSummary({ items, total }: { items: CartItem[]; total: number }) { + return ( +
+

장바구니

+
    + {items.map((item, index) => ( +
  • + {item.name} - {item.quantity} x {item.price} +
  • + ))} +
+

총합: {total}

+
+ ); +} +// ProductList 컴포넌트는 ShoppingCart에 느슨하게 결합됨 +``` + +```tsx title="고급 예시 - Pub/Sub 패턴 활용" +// ✅ 고급 예시 - Pub/Sub 패턴 활용 (예: mitt 라이브러리) +import mitt from 'mitt'; + +// 1. mitt를 이용한 이벤트 버스 생성 +type Events = { + addToCart: CartItem; +}; +const eventBus = mitt(); +// 2. 컴포넌트에서 mitt 이벤트 버스 사용 +function ShoppingCart() { + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + useEffect(() => { + const handleAddToCart = (item: CartItem) => { + setItems((prev) => [...prev, item]); + setTotal((prev) => prev + item.price * item.quantity); + }; + + eventBus.on('addToCart', handleAddToCart); + return () => { + eventBus.off('addToCart', handleAddToCart); + }; + }, []); + return ( +
+ + +
+ ); +} + +function ProductList() { + const products = useProducts(); + return ( +
+ {products.map((product) => ( +
+

{product.name}

+ +
+ ))} +
+ ); +} + +function CartSummary({ items, total }: { items: CartItem[]; total: number }) { + return ( +
+

장바구니

+
    + {items.map((item, index) => ( +
  • + {item.name} - {item.quantity} x {item.price} +
  • + ))} +
+

총합: {total}

+
+ ); +} +// ProductList 컴포넌트는 ShoppingCart에 느슨하게 결합됨 +``` From 3f8be0b0f647f63164c5c903146adde5613d2709 Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 14:59:09 +0900 Subject: [PATCH 07/16] =?UTF-8?q?docs(tech):=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...20\354\235\270\354\233\220\354\271\231.md" | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index 0e5da251..b93fcfa5 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -1625,3 +1625,23 @@ function CartSummary({ items, total }: { items: CartItem[]; total: number }) { } // ProductList 컴포넌트는 ShoppingCart에 느슨하게 결합됨 ``` + +## 정리 + +- **작게 나누기의 기준**은 재사용성·교체 가능성·독립 테스트 가치다. 기준을 충족하지 못하면 과분할을 피한다. +- **일관성은 객체/도메인이 책임**지고, UI는 의도만 전달한다(불변성·검증·캡슐화). +- **의존성은 인터페이스와 DI로 주입**하여 유연성과 테스트 용이성을 높인다(하드코딩 금지). +- **추상화 신호**(반복 수정, 비대한 컴포넌트, if 분기 증가)가 보이면 훅/HOC/도메인 서비스로 **관심사 분리**를 한다. +- **인프라 코드는 얇게** 만들고 도메인과 **명확히 분리**한다(Repository/Storage/Analytics 등 경계). +- **모듈화**는 이벤트/상태 관리로 **느슨한 결합**을 달성한다(콜백 의존 최소화). + +### 프론트엔드 적용 체크리스트 + +- [ ] 분리 전, 해당 조각이 재사용/교체/단위 테스트 가치가 있는가? +- [ ] 도메인 규칙이 컴포넌트가 아닌 서비스/훅에서 보장되는가? +- [ ] fetch/logger 등 외부 의존성을 DI/Context로 주입했는가? +- [ ] 로딩·에러·권한 체크 로직을 추상화/공통화했는가? +- [ ] API·스토리지·분석은 얇은 어댑터로 분리했는가? +- [ ] 컴포넌트 간 통신을 이벤트/상태로 처리해 결합을 낮췄는가? +- [ ] 불변성과 타입 안전성(제네릭/인터페이스)을 유지하는가? +- [ ] 테스트에서 의존성을 손쉽게 대체(모킹)할 수 있는가? From 147914dc3e79f1cff1172ff1eb51dfd1f547ce47 Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 15:06:07 +0900 Subject: [PATCH 08/16] chore: release v0.4.0 --- apps/frontend/tech/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/frontend/tech/package.json b/apps/frontend/tech/package.json index 1ad0a684..609a0c49 100644 --- a/apps/frontend/tech/package.json +++ b/apps/frontend/tech/package.json @@ -1,6 +1,6 @@ { "name": "tech", - "version": "0.3.1", + "version": "0.4.0", "private": true, "scripts": { "docusaurus": "docusaurus", From f870ce849b17559bd6970afbe9b5e6f93dddc598 Mon Sep 17 00:00:00 2001 From: 99mini <43674669+99mini@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:10:30 +0900 Subject: [PATCH 09/16] =?UTF-8?q?fix:=20=EC=98=A4=ED=83=88=EC=9E=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index b93fcfa5..1b1252d5 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -14,7 +14,7 @@ tags: [독서, 객체지향] ## 서문 -프론트엔드 개발자로 공부를 하면서 객체지향 프로그래밍을 적용하여 유자보수와 확장성을 높이는 방법을 고민하게 되었다. +프론트엔드 개발자로 공부를 하면서 객체지향 프로그래밍을 적용하여 유지보수와 확장성을 높이는 방법을 고민하게 되었다. 이 책은 백엔드를 기반으로 하여 (예제 코드가 Java 기반의 pseudo code로 작성되어 있음) 객체지향 시스템 디자인 원칙을 설명하고 있지만, 프론트엔드 개발자에게도 많은 도움이 될 것이라 생각되어 읽게 되었다. ## 책 소개 From a6e32d1cb58d5e88bc7c81f252ae2999be76bfb4 Mon Sep 17 00:00:00 2001 From: 99mini <43674669+99mini@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:10:48 +0900 Subject: [PATCH 10/16] =?UTF-8?q?fix:=20Update=20apps/frontend/tech/blog/2?= =?UTF-8?q?025-09-14-=EB=8F=85=EC=84=9C-=EA=B0=9D=EC=B2=B4=EC=A7=80?= =?UTF-8?q?=ED=96=A5=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=94=94=EC=9E=90=EC=9D=B8?= =?UTF-8?q?=EC=9B=90=EC=B9=99.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...24\224\354\236\220\354\235\270\354\233\220\354\271\231.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index 1b1252d5..dda2c462 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -580,12 +580,12 @@ function UserProfile({ userId }: { userId: string }) { > - 같은 클래스를 계속 반복적으로 수정하고 있는가? > - 클래스가 계속 커지고 있는가? > - 변화를 구현하기 위해 if 문을 계속 사용하는가? -> - 기존 비지니스 규칙을 시스템의 다른 부분에 결합시키는 과정이 어거지로 이어붙이는 것 같은가? +> - 기존 비즈니스 규칙을 시스템의 다른 부분에 결합시키는 과정이 억지로 이어붙이는 것 같은가? > > [P.136] 리액트 컴포넌트에서 반복적으로 사용되며 하나의 컴포넌트가 비대해지는 사례를 많이 만나볼 수 있다. -이 경우 추상화를 통해서 컴포넌트를 분리하거나 비지니스 로직을 커스텀 훅으로 분리하는 방법을 사용할 수 있다. +이 경우 추상화를 통해서 컴포넌트를 분리하거나 비즈니스 로직을 커스텀 훅으로 분리하는 방법을 사용할 수 있다. 혹은 BaseComponent를 만들어 공통된 로직을 처리하고, HOC(고차 컴포넌트)를 활용하여 추상화 할 수도 있다. --- From a3f36a94ca48af4153747d989566297324f8f410 Mon Sep 17 00:00:00 2001 From: 99mini <43674669+99mini@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:10:58 +0900 Subject: [PATCH 11/16] =?UTF-8?q?Update=20apps/frontend/tech/blog/2025-09-?= =?UTF-8?q?14-=EB=8F=85=EC=84=9C-=EA=B0=9D=EC=B2=B4=EC=A7=80=ED=96=A5?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=94=94=EC=9E=90=EC=9D=B8=EC=9B=90?= =?UTF-8?q?=EC=B9=99.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index dda2c462..5728b847 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -131,7 +131,7 @@ function UserAvatar({ avatar }) { 객체의 상태가 일관성을 유지하도록 하는 것은 매우 중요하다. 객체가 일관성을 유지하지 않으면, 예기치 않은 동작이 발생할 수 있다. 따라서 객체는 스스로 일관성을 책임지도록 설계해야 한다. -클리이언트는 절대 일관성 검사를 책임지지 말아야 하며, 기본적으로 클래스가 일관성을 관리해야 한다. 만약 클랙스에 들어가기에 검사가 너무 복잡하다면, 전체 작업을 서비스 클래스로 이동시키고 클라이언트가 원하는 동작을 위해 그 서비스 클래스를 사용해야 한다. [P.69] +클라이언트는 절대 일관성 검사를 책임지지 말아야 하며, 기본적으로 클래스가 일관성을 관리해야 한다. 만약 클래스에 들어가기에 검사가 너무 복잡하다면, 전체 작업을 서비스 클래스로 이동시키고 클라이언트가 원하는 동작을 위해 그 서비스 클래스를 사용해야 한다. [P.69] --- From bd6b3f16d4fa7344496e282d959e2f2dc29ce1b7 Mon Sep 17 00:00:00 2001 From: 99mini <43674669+99mini@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:11:06 +0900 Subject: [PATCH 12/16] =?UTF-8?q?Update=20apps/frontend/tech/blog/2025-09-?= =?UTF-8?q?14-=EB=8F=85=EC=84=9C-=EA=B0=9D=EC=B2=B4=EC=A7=80=ED=96=A5?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=94=94=EC=9E=90=EC=9D=B8=EC=9B=90?= =?UTF-8?q?=EC=B9=99.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index 5728b847..03de15c8 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -586,7 +586,7 @@ function UserProfile({ userId }: { userId: string }) { 리액트 컴포넌트에서 반복적으로 사용되며 하나의 컴포넌트가 비대해지는 사례를 많이 만나볼 수 있다. 이 경우 추상화를 통해서 컴포넌트를 분리하거나 비즈니스 로직을 커스텀 훅으로 분리하는 방법을 사용할 수 있다. -혹은 BaseComponent를 만들어 공통된 로직을 처리하고, HOC(고차 컴포넌트)를 활용하여 추상화 할 수도 있다. +혹은 BaseComponent를 만들어 공통된 로직을 처리하고, HOC(고차 컴포넌트)를 활용하여 추상화할 수도 있다. --- From 9d4118fea00dc9ec35bb976f34ab697a11dfcda7 Mon Sep 17 00:00:00 2001 From: 99mini <43674669+99mini@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:11:18 +0900 Subject: [PATCH 13/16] =?UTF-8?q?Update=20apps/frontend/tech/blog/2025-09-?= =?UTF-8?q?14-=EB=8F=85=EC=84=9C-=EA=B0=9D=EC=B2=B4=EC=A7=80=ED=96=A5?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=94=94=EC=9E=90=EC=9D=B8=EC=9B=90?= =?UTF-8?q?=EC=B9=99.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index 03de15c8..a02788dc 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -1354,7 +1354,7 @@ function OrderForm() { ### 모듈을 분리하는 방법으로 이벤트를 고려하라 -> 최근 이벤트 기반 아키텍처는 모듈과 서비스를 분리하는 놀라운 방법을 제공하여 인기을 얻고 있다. 이 아이디어는 모듈들이 후출을 통해 결합되는 대신, 무슨 일이 발생했는지 알라니는 이벤트를 발행하고, 관심 있는 모듈이 그 이벤트 스트림을 구독하는 것이다. +> 최근 이벤트 기반 아키텍처는 모듈과 서비스를 분리하는 놀라운 방법을 제공하여 인기을 얻고 있다. 이 아이디어는 모듈들이 호출을 통해 결합되는 대신, 무슨 일이 발생했는지 알리는 이벤트를 발행하고, 관심 있는 모듈이 그 이벤트 스트림을 구독하는 것이다. > > [P.176] From 612f27e8560fa5d465ebdebf51c210d6a49769f7 Mon Sep 17 00:00:00 2001 From: 99mini <43674669+99mini@users.noreply.github.com> Date: Sun, 14 Sep 2025 15:11:29 +0900 Subject: [PATCH 14/16] =?UTF-8?q?Update=20apps/frontend/tech/blog/2025-09-?= =?UTF-8?q?14-=EB=8F=85=EC=84=9C-=EA=B0=9D=EC=B2=B4=EC=A7=80=ED=96=A5?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=EB=94=94=EC=9E=90=EC=9D=B8=EC=9B=90?= =?UTF-8?q?=EC=B9=99.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ...\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index a02788dc..fa339ba9 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -1358,7 +1358,7 @@ function OrderForm() { > > [P.176] -이벤트 기반 아키텍쳐를 프론트엔드에 적용 하는 방법은 여러가지가 있다. +이벤트 기반 아키텍처를 프론트엔드에 적용하는 방법은 여러가지가 있다. - 이벤트 버스를 만들어서 컴포넌트 간에 이벤트를 발행하고 구독 - 상태 관리 라이브러리(Redux, Recoil, Zustand 등)의 구독 기능 활용 From 06cecf17514295f54da2333a57477cbf2a84afcf Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 15:15:19 +0900 Subject: [PATCH 15/16] =?UTF-8?q?fix:=20=EC=98=A4=ED=83=88=EC=9E=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\224\354\236\220\354\235\270\354\233\220\354\271\231.md" | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" index fa339ba9..0ccd5ddf 100644 --- "a/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" +++ "b/apps/frontend/tech/blog/2025-09-14-\353\217\205\354\204\234-\352\260\235\354\262\264\354\247\200\355\226\245\354\213\234\354\212\244\355\205\234\353\224\224\354\236\220\354\235\270\354\233\220\354\271\231.md" @@ -968,11 +968,11 @@ const UserProfile = withProtectedAsyncData(UserProfileComponent, { ### 도메인 코드와 인프라를 분리하라 -> 인프라를 다루는 코드는 도메인 코드와 분리해야 한다. 이런 클래스는 가능한 얇게 작성해야 하며, 비지니스 로직을 포함해서는 안 된다. 이렇게 분리하면 디자인이 깔끔하고 진화하기 쉬우며, 테스트를 쉽게 할 수 있다. +> 인프라를 다루는 코드는 도메인 코드와 분리해야 한다. 이런 클래스는 가능한 얇게 작성해야 하며, 비즈니스 로직을 포함해서는 안 된다. 이렇게 분리하면 디자인이 깔끔하고 진화하기 쉬우며, 테스트를 쉽게 할 수 있다. > > [P.141] -비지니스 로직이 포함된 클래스 안에 인프라를 다루는 코드를 작성하지 마라. [P.141] +비즈니스 로직이 포함된 클래스 안에 인프라를 다루는 코드를 작성하지 마라. [P.141] ```tsx title="잘못된 예시 - 도메인 로직과 인프라 코드가 섞임" // ❌ 잘못된 예시 - 비즈니스 로직과 인프라 코드가 한 곳에 @@ -1354,7 +1354,7 @@ function OrderForm() { ### 모듈을 분리하는 방법으로 이벤트를 고려하라 -> 최근 이벤트 기반 아키텍처는 모듈과 서비스를 분리하는 놀라운 방법을 제공하여 인기을 얻고 있다. 이 아이디어는 모듈들이 호출을 통해 결합되는 대신, 무슨 일이 발생했는지 알리는 이벤트를 발행하고, 관심 있는 모듈이 그 이벤트 스트림을 구독하는 것이다. +> 최근 이벤트 기반 아키텍처는 모듈과 서비스를 분리하는 놀라운 방법을 제공하여 인기를 얻고 있다. 이 아이디어는 모듈들이 호출을 통해 결합되는 대신, 무슨 일이 발생했는지 알리는 이벤트를 발행하고, 관심 있는 모듈이 그 이벤트 스트림을 구독하는 것이다. > > [P.176] From a47e3b707d30a696ff49f989b86b70458b4ba590 Mon Sep 17 00:00:00 2001 From: ym-mac <0mini9939@gmail.com> Date: Sun, 14 Sep 2025 15:24:53 +0900 Subject: [PATCH 16/16] fix: broken anchor --- apps/frontend/tech/blog/tags.yml | 15 +++++++++++++++ apps/frontend/tech/docs/reactjs/virtual-dom.md | 2 +- .../RESTful.md" | 2 +- .../IPC.md" | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apps/frontend/tech/blog/tags.yml b/apps/frontend/tech/blog/tags.yml index f04d0771..c9defc4b 100644 --- a/apps/frontend/tech/blog/tags.yml +++ b/apps/frontend/tech/blog/tags.yml @@ -97,3 +97,18 @@ ui-ux: label: 구현 permalink: /구현 description: 구현 + +독서: + label: 독서 + permalink: /독서 + description: 기술 서적 독서 + +객체지향: + label: 객체지향 + permalink: /객체지향 + description: 객체지향 프로그래밍 + +oop: + label: OOP + permalink: /oop + description: 객체지향 프로그래밍 diff --git a/apps/frontend/tech/docs/reactjs/virtual-dom.md b/apps/frontend/tech/docs/reactjs/virtual-dom.md index b9be741b..fef316e4 100644 --- a/apps/frontend/tech/docs/reactjs/virtual-dom.md +++ b/apps/frontend/tech/docs/reactjs/virtual-dom.md @@ -20,7 +20,7 @@ DOM 문서는 논리적으로 트리 형태를 가지며 각 노드는 객체를 ## Virtual DOM이란? Virtual DOM(가상 DOM)은 UI 업데이트의 효율성을 극대화하기 위해 실제 DOM(Document Object Model)의 가벼운 복사본을 메모리 상에서 유지 관리하는 기술입니다. DOM 조작이 느린 브라우저 환경에서 성능을 개선하는 데 중요한 역할을 수행합니다. [[2]](#react와-춤을) -UI의 이상적인 또는 **“가상”** 적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 **“실제”** DOM과 동기화하는 프로그래밍 개념입니다. 이 과정을 *재조정*이라고 합니다. [[3]](#react공식문서-virtual-dom과-internals) +UI의 이상적인 또는 **“가상”** 적인 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 **“실제”** DOM과 동기화하는 프로그래밍 개념입니다. 이 과정을 *재조정*이라고 합니다. [[3]](#react-공식-문서---virtual-dom과-internals) ## Virtual DOM의 동작 방식 diff --git "a/apps/frontend/tech/docs/\353\204\244\355\212\270\354\233\214\355\201\254/RESTful.md" "b/apps/frontend/tech/docs/\353\204\244\355\212\270\354\233\214\355\201\254/RESTful.md" index 59a52f67..5f007f1f 100644 --- "a/apps/frontend/tech/docs/\353\204\244\355\212\270\354\233\214\355\201\254/RESTful.md" +++ "b/apps/frontend/tech/docs/\353\204\244\355\212\270\354\233\214\355\201\254/RESTful.md" @@ -11,7 +11,7 @@ RESTFUL 개념 -RESTful 웹 API 구현은 REST(Representational State Transfer) 아키텍처 원칙을 사용하여 클라이언트와 서비스 간에 느슨하게 결합된 상태 비 상태 인터페이스를 달성하는 웹 API입니다. RESTful인 웹 API는 표준 HTTP 프로토콜을 지원하여 리소스에 대한 작업을 수행하고 하이퍼미디어 링크 및 HTTP 작업 상태 코드를 포함하는 리소스의 표현을 반환합니다. [[1]](#microsoft---restful-api) +RESTful 웹 API 구현은 REST(Representational State Transfer) 아키텍처 원칙을 사용하여 클라이언트와 서비스 간에 느슨하게 결합된 상태 비 상태 인터페이스를 달성하는 웹 API입니다. RESTful인 웹 API는 표준 HTTP 프로토콜을 지원하여 리소스에 대한 작업을 수행하고 하이퍼미디어 링크 및 HTTP 작업 상태 코드를 포함하는 리소스의 표현을 반환합니다. [[1]](#1-microsoft---restful-api) ### RESTful API의 원칙 diff --git "a/apps/frontend/tech/docs/\354\232\264\354\230\201\354\262\264\354\240\234/IPC.md" "b/apps/frontend/tech/docs/\354\232\264\354\230\201\354\262\264\354\240\234/IPC.md" index f3d5749e..4daf5ded 100644 --- "a/apps/frontend/tech/docs/\354\232\264\354\230\201\354\262\264\354\240\234/IPC.md" +++ "b/apps/frontend/tech/docs/\354\232\264\354\230\201\354\262\264\354\240\234/IPC.md" @@ -13,7 +13,7 @@ date: 2025-07-27 -프로세스 간 통신(Inter-Process Communication, IPC)이란 프로세스들 사이에 서로 데이터를 주고받는 행위 또는 그에 대한 방법이나 경로를 뜻한다. [[1]](#1-wiki-ipc) +프로세스 간 통신(Inter-Process Communication, IPC)이란 프로세스들 사이에 서로 데이터를 주고받는 행위 또는 그에 대한 방법이나 경로를 뜻한다. [[1]](#1-wiki---ipc) ### 주요 IPC 방식