From fa4f4e1cebcc573b5601cd2ac78ab56dc12d1c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8E=E1=85=A5=E1=86=AB=E1=84=8C=E1=85=B5=E1=86=AB?= =?UTF-8?q?=E1=84=8B=E1=85=A1?= Date: Mon, 6 Apr 2026 22:38:23 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EB=A9=94?= =?UTF-8?q?=EC=86=8C=EB=93=9C=20=ED=8C=A8=ED=84=B4=20=EC=A0=95=EB=A6=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...4\353\223\234_\355\214\250\355\204\264.md" | 1160 +++++++++++++++++ 1 file changed, 1160 insertions(+) create mode 100644 "jina/08.\355\205\234\355\224\214\353\246\277_\353\251\224\354\206\214\353\223\234_\355\214\250\355\204\264.md" diff --git "a/jina/08.\355\205\234\355\224\214\353\246\277_\353\251\224\354\206\214\353\223\234_\355\214\250\355\204\264.md" "b/jina/08.\355\205\234\355\224\214\353\246\277_\353\251\224\354\206\214\353\223\234_\355\214\250\355\204\264.md" new file mode 100644 index 0000000..ea39544 --- /dev/null +++ "b/jina/08.\355\205\234\355\224\214\353\246\277_\353\251\224\354\206\214\353\223\234_\355\214\250\355\204\264.md" @@ -0,0 +1,1160 @@ +# 템플릿 메소드 패턴 (Template Method Pattern) + +## 패턴 정의 + +**템플릿 메소드 패턴**은 알고리즘의 골격을 정의하는 디자인 패턴. 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의 가능 + +## 문제 상황 + +커피와 홍차를 만드는 과정을 생각해보자. + +### 기본 구현 (리팩토링 전) + +```typescript +class Coffee { + prepareRecipe() { + this.boilWater(); + this.brewCoffeeGrinds(); + this.pourInCup(); + this.addSugarAndMilk(); + } + + boilWater() { + console.log('물 끓이는 중'); + } + + brewCoffeeGrinds() { + console.log('커피 우려내는 중'); + } + + pourInCup() { + console.log('컵에 따르는 중'); + } + + addSugarAndMilk() { + console.log('설탕과 우유를 추가하는 중'); + } +} + +class Tea { + prepareRecipe() { + this.boilWater(); + this.steepTeaBag(); + this.pourInCup(); + this.addLemon(); + } + + boilWater() { + console.log('물 끓이는 중'); + } + + steepTeaBag() { + console.log('찻잎을 우려내는 중'); + } + + addLemon() { + console.log('레몬을 추가하는 중'); + } + + pourInCup() { + console.log('컵에 따르는 중'); + } +} +``` + +### 문제점 + +- 코드 중복 (`boilWater()`, `pourInCup()`) +- 알고리즘 구조가 각 클래스에 분산되어 있음 + +## 해결 방법: 알고리즘 추상화 + +커피와 홍차의 제조 과정은 동일한 알고리즘을 따름: + +1. 물을 끓인다 +2. 뜨거운 물을 사용해서 커피 또는 찻잎을 우려낸다 +3. 만들어진 음료를 컵에 따른다 +4. 각 음료에 맞는 첨가물을 추가한다 + +### 템플릿 메소드 패턴 적용 + +```typescript +abstract class CaffeineBeverage { + // 템플릿 메소드: 알고리즘의 골격을 정의 + prepareRecipe() { + this.boilWater(); + this.brew(); + this.pourInCup(); + this.addCondiments(); + } + + // 서브클래스에서 구현해야 하는 추상 메소드 + abstract brew(): void; + abstract addCondiments(): void; + + // 공통 구현 + boilWater() { + console.log('물 끓이는 중'); + } + + pourInCup() { + console.log('컵에 따르는 중'); + } +} + +class Tea extends CaffeineBeverage { + brew(): void { + console.log('찻잎을 우려내는 중'); + } + + addCondiments(): void { + console.log('레몬을 추가하는 중'); + } +} + +class Coffee extends CaffeineBeverage { + brew(): void { + console.log('필터로 커피를 우려내는 중'); + } + + addCondiments(): void { + console.log('설탕과 우유를 추가하는 중'); + } +} +``` + +## 패턴 구조 + +```typescript +abstract class AbstractClass { + // 템플릿 메소드: 알고리즘의 골격 + templateMethod() { + this.primitiveOperation1(); + this.primitiveOperation2(); + this.concreteOperation(); + this.hook(); + } + + // 추상 단계: 서브클래스에서 구현 + abstract primitiveOperation1(): void; + abstract primitiveOperation2(): void; + + // 구상 단계: 추상 클래스 내에서 정의 + concreteOperation(): void { + console.log('concreteOperation'); + } + + // 후크: 서브클래스에서 선택적으로 오버라이드 + hook(): void {} +} + +class ConcreteClass extends AbstractClass { + primitiveOperation1() { + console.log('primitiveOperation1'); + } + + primitiveOperation2() { + console.log('primitiveOperation2'); + } +} +``` + +## 후크(Hook) + +**후크**는 추상 클래스에서 선언되지만 기본적인 내용만 구현되어 있거나 아무 코드도 들어있지 않은 메소드. 서브클래스가 다양한 위치에서 알고리즘에 끼어들 수 있게 해줌 + +### 후크 사용 예제 + +```typescript +abstract class CaffeineBeverageWithHook { + prepareRecipe() { + this.boilWater(); + this.brew(); + this.pourInCup(); + // 후크를 사용한 조건부 실행 + if (this.customerWantsCondiments()) { + this.addCondiments(); + } + } + + abstract brew(): void; + abstract addCondiments(): void; + + boilWater() { + console.log('물 끓이는 중'); + } + + pourInCup() { + console.log('컵에 따르는 중'); + } + + // 후크: 서브클래스에서 필요할 때 오버라이드 + customerWantsCondiments(): boolean { + return true; + } +} + +class CoffeeWithHook extends CaffeineBeverageWithHook { + brew(): void { + console.log('필터로 커피를 우려내는 중'); + } + + addCondiments(): void { + console.log('설탕과 우유를 추가하는 중'); + } + + // 후크 오버라이드 + customerWantsCondiments(): boolean { + const answer = this.getUserInput(); + return answer ? true : false; + } + + getUserInput() { + return window.confirm('커피에 우유와 설탕을 넣을까요?'); + } +} +``` + +### 추상 메소드 vs 후크 + +- **추상 메소드**: 서브클래스가 알고리즘의 특정 단계를 **반드시** 제공해야 할 때 사용 +- **후크**: 알고리즘의 특정 단계가 **선택적**으로 적용될 때 사용 + +## 할리우드 원칙 + +> "먼저 연락하지 마세요. 저희가 연락 드리겠습니다." + +### 의존성 부패 방지 + +할리우드 원칙을 활용하면 **의존성 부패**를 방지할 수 있음 + +- **의존성 부패**: 고수준 구성요소 → 저수준 구성요소에 의존 → 고수준 구성요소에 의존 등 의존성이 복잡하게 꼬여있는 상황 +- **할리우드 원칙**: 저수준 구성요소가 시스템에 접속할 수는 있지만, 언제, 어떻게 그 구성요소를 사용할지는 고수준 구성요소가 결정 + +### 템플릿 메소드 패턴과의 관계 + +- `CaffeineBeverage`는 **고수준 구성요소**: 음료를 만드는 알고리즘을 장악 +- 서브클래스는 **저수준 구성요소**: 메소드 구현 제공 용도로만 사용 +- 서브클래스는 **호출 당하기 전**까지는 추상 클래스를 직접 호출하지 않음 + +## 패턴의 장점 + +1. **코드 재사용**: 서브클래스에서 공통 코드를 재사용 가능 +2. **알고리즘 집중화**: 알고리즘이 한 곳에 모여 있어 한 부분만 고치면 됨 +3. **확장성**: 다른 음료(서브클래스)를 쉽게 추가할 수 있는 프레임워크 제공 +4. **제어 역전**: 추상 클래스가 알고리즘을 독점하고 서브클래스는 일부만 구현 +5. **유지보수성**: 알고리즘 지식이 추상 클래스에 집중되어 있음 + +## 다른 패턴과의 비교 + +### 템플릿 메소드 패턴 + +- 알고리즘의 어떤 단계를 구현하는 방법을 서브클래스에서 결정 + +### 전략 패턴 + +- 바꿔 쓸 수 있는 행동을 캡슐화하고, 어떤 행동을 사용할지는 서브클래스에 맡김 + +### 팩토리 메소드 패턴 + +- 구상 클래스의 인스턴스 생성을 서브클래스에서 결정 + +## 프론트엔드 실제 사용 예제 + +### 1. 데이터 로딩 컴포넌트 + +다양한 데이터 소스에서 데이터를 가져오는 컴포넌트의 공통 로직 + +```typescript +abstract class DataLoader { + // 템플릿 메소드: 데이터 로딩 프로세스 + async loadData(): Promise { + try { + this.showLoading(); + await this.validateParams(); + const data = await this.fetchData(); + const processedData = await this.processData(data); + this.renderData(processedData); + this.onSuccess(); + } catch (error) { + this.handleError(error); + } finally { + this.hideLoading(); + } + } + + // 공통 구현 + showLoading() { + console.log('로딩 중...'); + } + + hideLoading() { + console.log('로딩 완료'); + } + + // 서브클래스에서 구현 + abstract validateParams(): Promise; + abstract fetchData(): Promise; + abstract processData(data: T): Promise; + abstract renderData(data: T): void; + + // 후크 메소드 + onSuccess(): void { + console.log('데이터 로딩 성공'); + } + + handleError(error: any): void { + console.error('에러 발생:', error); + } +} + +// API 데이터 로더 +class ApiDataLoader extends DataLoader { + constructor(private apiUrl: string) { + super(); + } + + async validateParams(): Promise { + if (!this.apiUrl) { + throw new Error('API URL이 필요합니다'); + } + } + + async fetchData(): Promise { + const response = await fetch(this.apiUrl); + return response.json(); + } + + async processData(data: any[]): Promise { + // 데이터 필터링 및 정렬 + return data.filter((item) => item.active).sort((a, b) => a.id - b.id); + } + + renderData(data: any[]): void { + console.log('API 데이터 렌더링:', data); + // DOM 업데이트 로직 + } +} + +// LocalStorage 데이터 로더 +class LocalStorageLoader extends DataLoader { + constructor(private key: string) { + super(); + } + + async validateParams(): Promise { + if (!this.key) { + throw new Error('Storage key가 필요합니다'); + } + } + + async fetchData(): Promise { + const data = localStorage.getItem(this.key); + if (!data) throw new Error('데이터가 없습니다'); + return data; + } + + async processData(data: string): Promise { + return JSON.parse(data); + } + + renderData(data: any): void { + console.log('LocalStorage 데이터 렌더링:', data); + } + + // 후크 오버라이드 + onSuccess(): void { + console.log('캐시 데이터 로딩 완료'); + } +} + +// 사용 예시 +const apiLoader = new ApiDataLoader('https://api.example.com/users'); +apiLoader.loadData(); + +const storageLoader = new LocalStorageLoader('user-data'); +storageLoader.loadData(); +``` + +### 2. Form 유효성 검사 + +여러 종류의 폼에서 공통 유효성 검사 프로세스 + +```typescript +abstract class FormValidator { + // 템플릿 메소드: 폼 제출 프로세스 + async submitForm(formData: any): Promise { + if (!this.validateRequired(formData)) { + return false; + } + + if (!this.validateFormat(formData)) { + return false; + } + + if (!(await this.validateCustomRules(formData))) { + return false; + } + + if (this.shouldConfirm()) { + const confirmed = await this.showConfirmation(); + if (!confirmed) return false; + } + + return await this.submit(formData); + } + + // 공통 구현 + validateRequired(formData: any): boolean { + const requiredFields = this.getRequiredFields(); + for (const field of requiredFields) { + if (!formData[field]) { + this.showError(`${field}는 필수 항목입니다`); + return false; + } + } + return true; + } + + showError(message: string): void { + console.error(message); + } + + // 서브클래스에서 구현 + abstract getRequiredFields(): string[]; + abstract validateFormat(formData: any): boolean; + abstract validateCustomRules(formData: any): Promise; + abstract submit(formData: any): Promise; + + // 후크 메소드 + shouldConfirm(): boolean { + return false; + } + + async showConfirmation(): Promise { + return confirm('제출하시겠습니까?'); + } +} + +// 회원가입 폼 +class SignupFormValidator extends FormValidator { + getRequiredFields(): string[] { + return ['email', 'password', 'username']; + } + + validateFormat(formData: any): boolean { + // 이메일 형식 검증 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + this.showError('올바른 이메일 형식이 아닙니다'); + return false; + } + + // 비밀번호 강도 검증 + if (formData.password.length < 8) { + this.showError('비밀번호는 8자 이상이어야 합니다'); + return false; + } + + return true; + } + + async validateCustomRules(formData: any): Promise { + // 이메일 중복 체크 + const response = await fetch(`/api/check-email?email=${formData.email}`); + const { exists } = await response.json(); + + if (exists) { + this.showError('이미 사용 중인 이메일입니다'); + return false; + } + + return true; + } + + async submit(formData: any): Promise { + const response = await fetch('/api/signup', { + method: 'POST', + body: JSON.stringify(formData), + }); + return response.ok; + } +} + +// 결제 폼 +class PaymentFormValidator extends FormValidator { + getRequiredFields(): string[] { + return ['cardNumber', 'cvv', 'expiryDate']; + } + + validateFormat(formData: any): boolean { + // 카드 번호 검증 + const cardRegex = /^\d{16}$/; + if (!cardRegex.test(formData.cardNumber.replace(/\s/g, ''))) { + this.showError('올바른 카드 번호가 아닙니다'); + return false; + } + + return true; + } + + async validateCustomRules(formData: any): Promise { + // 카드 유효성 API 체크 + return true; + } + + async submit(formData: any): Promise { + const response = await fetch('/api/payment', { + method: 'POST', + body: JSON.stringify(formData), + }); + return response.ok; + } + + // 후크 오버라이드 + shouldConfirm(): boolean { + return true; // 결제는 항상 확인 필요 + } + + async showConfirmation(): Promise { + return confirm('결제를 진행하시겠습니까?'); + } +} +``` + +### 3. React 컴포넌트 렌더링 패턴 + +```typescript +import React from 'react'; + +abstract class BasePageComponent extends React.Component { + constructor(props: any) { + super(props); + this.state = { + data: null, + loading: true, + error: null + }; + } + + // 템플릿 메소드: 컴포넌트 렌더링 프로세스 + render() { + return ( +
+ {this.renderHeader()} + {this.renderContent()} + {this.renderFooter()} +
+ ); + } + + renderContent() { + if (this.state.loading) { + return this.renderLoading(); + } + + if (this.state.error) { + return this.renderError(); + } + + if (!this.state.data) { + return this.renderEmpty(); + } + + return this.renderData(this.state.data); + } + + // 공통 구현 + renderLoading() { + return
로딩 중...
; + } + + renderError() { + return
{this.state.error}
; + } + + renderEmpty() { + return
데이터가 없습니다
; + } + + // 서브클래스에서 구현 + abstract renderHeader(): React.ReactNode; + abstract renderData(data: T): React.ReactNode; + abstract renderFooter(): React.ReactNode; + abstract getContainerClassName(): string; + + // 후크 메소드 + componentDidMount() { + this.loadData(); + } + + async loadData() { + // 서브클래스에서 구현 가능 + } +} + +// 사용자 목록 페이지 +class UserListPage extends BasePageComponent { + renderHeader() { + return

사용자 목록

; + } + + renderData(users: any[]) { + return ( +
    + {users.map(user => ( +
  • {user.name}
  • + ))} +
+ ); + } + + renderFooter() { + return
총 {this.state.data?.length}명
; + } + + getContainerClassName() { + return 'user-list-page'; + } + + async loadData() { + try { + const response = await fetch('/api/users'); + const users = await response.json(); + this.setState({ data: users, loading: false }); + } catch (error) { + this.setState({ error: '데이터 로딩 실패', loading: false }); + } + } +} +``` + +### 4. 애니메이션 시퀀스 + +```typescript +abstract class AnimationSequence { + // 템플릿 메소드: 애니메이션 실행 프로세스 + async play(element: HTMLElement): Promise { + await this.beforeAnimation(element); + await this.fadeIn(element); + await this.mainAnimation(element); + await this.fadeOut(element); + await this.afterAnimation(element); + } + + // 공통 구현 + async fadeIn(element: HTMLElement): Promise { + element.style.opacity = '0'; + element.style.transition = 'opacity 0.3s'; + + await new Promise((resolve) => setTimeout(resolve, 50)); + element.style.opacity = '1'; + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + async fadeOut(element: HTMLElement): Promise { + element.style.transition = 'opacity 0.3s'; + element.style.opacity = '0'; + await new Promise((resolve) => setTimeout(resolve, 300)); + } + + // 서브클래스에서 구현 + abstract mainAnimation(element: HTMLElement): Promise; + + // 후크 메소드 + async beforeAnimation(element: HTMLElement): Promise { + // 필요시 오버라이드 + } + + async afterAnimation(element: HTMLElement): Promise { + // 필요시 오버라이드 + } +} + +// 슬라이드 애니메이션 +class SlideAnimation extends AnimationSequence { + async mainAnimation(element: HTMLElement): Promise { + element.style.transform = 'translateX(-100%)'; + element.style.transition = 'transform 0.5s ease'; + + await new Promise((resolve) => setTimeout(resolve, 50)); + element.style.transform = 'translateX(0)'; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + async beforeAnimation(element: HTMLElement): Promise { + element.style.transform = 'translateX(-100%)'; + } +} + +// 바운스 애니메이션 +class BounceAnimation extends AnimationSequence { + async mainAnimation(element: HTMLElement): Promise { + element.style.transition = 'transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)'; + element.style.transform = 'scale(1.2)'; + + await new Promise((resolve) => setTimeout(resolve, 300)); + element.style.transform = 'scale(1)'; + await new Promise((resolve) => setTimeout(resolve, 300)); + } +} + +// 사용 예시 +const slideAnim = new SlideAnimation(); +const element = document.querySelector('.card') as HTMLElement; +slideAnim.play(element); +``` + +## 객체 기반 구현 (Class 없이) + +TypeScript/JavaScript에서 클래스 없이 객체와 함수 조합으로 템플릿 메소드 패턴을 구현할 수 있음 + +### 1. Factory 함수 패턴 + +```typescript +// 템플릿 메소드를 가진 베이스 객체 생성 함수 +function createDataLoader(config: { + validateParams: () => Promise; + fetchData: () => Promise; + processData: (data: any) => Promise; + renderData: (data: any) => void; + onSuccess?: () => void; + handleError?: (error: any) => void; +}) { + return { + // 템플릿 메소드 + async loadData() { + try { + this.showLoading(); + await config.validateParams(); + const data = await config.fetchData(); + const processedData = await config.processData(data); + config.renderData(processedData); + if (config.onSuccess) config.onSuccess(); + } catch (error) { + if (config.handleError) { + config.handleError(error); + } else { + console.error('에러 발생:', error); + } + } finally { + this.hideLoading(); + } + }, + + // 공통 메소드 + showLoading() { + console.log('로딩 중...'); + }, + + hideLoading() { + console.log('로딩 완료'); + }, + }; +} + +// API 데이터 로더 생성 +const apiLoader = createDataLoader({ + async validateParams() { + if (!apiUrl) throw new Error('API URL이 필요합니다'); + }, + async fetchData() { + const response = await fetch('https://api.example.com/users'); + return response.json(); + }, + async processData(data) { + return data.filter((item) => item.active); + }, + renderData(data) { + console.log('API 데이터 렌더링:', data); + }, + onSuccess() { + console.log('API 데이터 로딩 성공'); + }, +}); + +apiLoader.loadData(); + +// LocalStorage 로더 생성 +const storageLoader = createDataLoader({ + async validateParams() { + if (!key) throw new Error('Storage key가 필요합니다'); + }, + async fetchData() { + const data = localStorage.getItem('user-data'); + if (!data) throw new Error('데이터가 없습니다'); + return data; + }, + async processData(data) { + return JSON.parse(data); + }, + renderData(data) { + console.log('LocalStorage 데이터 렌더링:', data); + }, +}); + +storageLoader.loadData(); +``` + +### 2. 고차 함수 패턴 + +```typescript +// 템플릿 메소드를 반환하는 함수 +function createFormSubmitter(options: { + requiredFields: string[]; + validateFormat: (formData: any) => boolean; + validateCustomRules: (formData: any) => Promise; + submit: (formData: any) => Promise; + shouldConfirm?: boolean; +}) { + // 공통 함수들 + const showError = (message: string) => { + console.error(message); + }; + + const validateRequired = (formData: any): boolean => { + for (const field of options.requiredFields) { + if (!formData[field]) { + showError(`${field}는 필수 항목입니다`); + return false; + } + } + return true; + }; + + // 템플릿 메소드 반환 + return async (formData: any): Promise => { + if (!validateRequired(formData)) { + return false; + } + + if (!options.validateFormat(formData)) { + return false; + } + + if (!(await options.validateCustomRules(formData))) { + return false; + } + + if (options.shouldConfirm) { + const confirmed = confirm('제출하시겠습니까?'); + if (!confirmed) return false; + } + + return await options.submit(formData); + }; +} + +// 회원가입 폼 제출 함수 +const submitSignupForm = createFormSubmitter({ + requiredFields: ['email', 'password', 'username'], + + validateFormat(formData) { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + console.error('올바른 이메일 형식이 아닙니다'); + return false; + } + return true; + }, + + async validateCustomRules(formData) { + const response = await fetch(`/api/check-email?email=${formData.email}`); + const { exists } = await response.json(); + if (exists) { + console.error('이미 사용 중인 이메일입니다'); + return false; + } + return true; + }, + + async submit(formData) { + const response = await fetch('/api/signup', { + method: 'POST', + body: JSON.stringify(formData), + }); + return response.ok; + }, +}); + +// 사용 +submitSignupForm({ email: 'test@example.com', password: '12345678', username: 'user' }); + +// 결제 폼 제출 함수 +const submitPaymentForm = createFormSubmitter({ + requiredFields: ['cardNumber', 'cvv', 'expiryDate'], + shouldConfirm: true, + + validateFormat(formData) { + const cardRegex = /^\d{16}$/; + if (!cardRegex.test(formData.cardNumber.replace(/\s/g, ''))) { + console.error('올바른 카드 번호가 아닙니다'); + return false; + } + return true; + }, + + async validateCustomRules(formData) { + return true; + }, + + async submit(formData) { + const response = await fetch('/api/payment', { + method: 'POST', + body: JSON.stringify(formData), + }); + return response.ok; + }, +}); +``` + +### 3. Composition 패턴 + +```typescript +// 공통 기능들 +const loaderCommon = { + showLoading() { + console.log('로딩 중...'); + }, + hideLoading() { + console.log('로딩 완료'); + }, +}; + +// 템플릿 메소드 +const createLoader = (implementation: { + validate: () => Promise; + fetch: () => Promise; + process: (data: any) => Promise; + render: (data: any) => void; +}) => ({ + ...loaderCommon, + + async load() { + try { + this.showLoading(); + await implementation.validate(); + const data = await implementation.fetch(); + const processed = await implementation.process(data); + implementation.render(processed); + } catch (error) { + console.error('에러:', error); + } finally { + this.hideLoading(); + } + }, +}); + +// 구체적인 로더들 +const apiLoader = createLoader({ + async validate() { + console.log('API 파라미터 검증'); + }, + async fetch() { + return fetch('/api/data').then((r) => r.json()); + }, + async process(data) { + return data.filter((item) => item.active); + }, + render(data) { + console.log('렌더링:', data); + }, +}); + +apiLoader.load(); +``` + +### 4. React Hooks 패턴 + +```typescript +// 커스텀 훅으로 템플릿 메소드 구현 +function useDataLoader(config: { + fetchData: () => Promise; + processData: (data: T) => T; + onSuccess?: () => void; + onError?: (error: Error) => void; +}) { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // 템플릿 메소드 + const loadData = async () => { + try { + setLoading(true); + setError(null); + + const rawData = await config.fetchData(); + const processedData = config.processData(rawData); + + setData(processedData); + config.onSuccess?.(); + } catch (err) { + const error = err as Error; + setError(error); + config.onError?.(error); + } finally { + setLoading(false); + } + }; + + return { data, loading, error, loadData }; +} + +// 사용 예시 1: API 데이터 +function UserListComponent() { + const { data, loading, error, loadData } = useDataLoader({ + fetchData: async () => { + const response = await fetch('/api/users'); + return response.json(); + }, + processData: (users) => { + return users.filter(user => user.active); + }, + onSuccess: () => { + console.log('사용자 목록 로딩 완료'); + } + }); + + useEffect(() => { + loadData(); + }, []); + + if (loading) return
로딩 중...
; + if (error) return
에러: {error.message}
; + + return ( +
    + {data?.map(user =>
  • {user.name}
  • )} +
+ ); +} + +// 사용 예시 2: LocalStorage 데이터 +function CachedDataComponent() { + const { data, loading, loadData } = useDataLoader({ + fetchData: async () => { + const cached = localStorage.getItem('cached-data'); + if (!cached) throw new Error('캐시 없음'); + return JSON.parse(cached); + }, + processData: (data) => { + return data; // 추가 처리 없음 + }, + onSuccess: () => { + console.log('캐시 데이터 로딩 완료'); + } + }); + + useEffect(() => { + loadData(); + }, []); + + return
{JSON.stringify(data)}
; +} +``` + +### 5. 순수 함수 조합 패턴 + +```typescript +// 템플릿 메소드를 순수 함수로 구현 +const createAnimationPlayer = (animations: { + main: (element: HTMLElement) => Promise; + before?: (element: HTMLElement) => Promise; + after?: (element: HTMLElement) => Promise; +}) => { + // 공통 애니메이션 함수들 + const fadeIn = async (element: HTMLElement) => { + element.style.opacity = '0'; + element.style.transition = 'opacity 0.3s'; + await new Promise((resolve) => setTimeout(resolve, 50)); + element.style.opacity = '1'; + await new Promise((resolve) => setTimeout(resolve, 300)); + }; + + const fadeOut = async (element: HTMLElement) => { + element.style.transition = 'opacity 0.3s'; + element.style.opacity = '0'; + await new Promise((resolve) => setTimeout(resolve, 300)); + }; + + // 템플릿 메소드 (순수 함수) + return async (element: HTMLElement) => { + if (animations.before) { + await animations.before(element); + } + await fadeIn(element); + await animations.main(element); + await fadeOut(element); + if (animations.after) { + await animations.after(element); + } + }; +}; + +// 슬라이드 애니메이션 +const playSlideAnimation = createAnimationPlayer({ + before: async (element) => { + element.style.transform = 'translateX(-100%)'; + }, + main: async (element) => { + element.style.transition = 'transform 0.5s ease'; + await new Promise((resolve) => setTimeout(resolve, 50)); + element.style.transform = 'translateX(0)'; + await new Promise((resolve) => setTimeout(resolve, 500)); + }, +}); + +// 바운스 애니메이션 +const playBounceAnimation = createAnimationPlayer({ + main: async (element) => { + element.style.transition = 'transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55)'; + element.style.transform = 'scale(1.2)'; + await new Promise((resolve) => setTimeout(resolve, 300)); + element.style.transform = 'scale(1)'; + await new Promise((resolve) => setTimeout(resolve, 300)); + }, +}); + +// 사용 +const element = document.querySelector('.card') as HTMLElement; +playSlideAnimation(element); +``` + +### 클래스 vs 객체 비교 + +| 특징 | 클래스 기반 | 객체 기반 | +| --------------------- | ----------------------- | ----------------------------- | +| **문법** | 상속, abstract | 함수, 객체 조합 | +| **타입 안정성** | 강함 (추상 메소드 강제) | 약함 (인터페이스로 보완 가능) | +| **유연성** | 낮음 (상속 구조 고정) | 높음 (런타임 조합 가능) | +| **테스트** | Mock/Spy 복잡 | 함수 단위 테스트 쉬움 | +| **번들 크기** | 클래스 메타데이터 포함 | 더 작음 | +| **함수형 프로그래밍** | 어려움 | 쉬움 (조합, 커링 등) | +| **사용 사례** | 대규모 앱, 엄격한 구조 | 작은 유틸리티, 유연한 구조 | + +### 선택 가이드 + +**클래스를 사용하는 경우:** + +- 타입 안정성이 중요할 때 +- 상속 구조가 명확할 때 +- 팀이 OOP에 익숙할 때 +- 대규모 애플리케이션 + +**객체를 사용하는 경우:** + +- 유연성이 중요할 때 +- 함수형 프로그래밍 선호 +- 작은 유틸리티 함수 +- 번들 크기 최적화가 중요할 때 +- React Hooks 환경 + +## 사용 시나리오 + +템플릿 메소드 패턴은 다음과 같은 경우에 유용함: + +- 여러 클래스가 동일한 알고리즘 구조를 공유하지만 일부 단계의 구현이 다를 때 +- 공통 로직을 추상 클래스에 모으고 변경되는 부분만 서브클래스에서 구현하고 싶을 때 +- 알고리즘의 특정 단계를 서브클래스에서 커스터마이징하도록 허용하고 싶을 때 +- 코드 중복을 줄이고 재사용성을 높이고 싶을 때