Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions doeun/08.템플릿_메소드_패턴.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
# 템플릿 메소드 패턴 (Template Method Pattern)

## 1. 들어가며

비슷한 작업 반복 작성 경험. API 호출 → 로딩 → 에러 처리 → 데이터 변환 → 렌더링. 매번 같은 흐름, 세부만 다름. 복붙하다 보면 흐름이 어긋나고 버그 생김.

이런 상황에서 "전체 흐름은 고정, 일부 단계만 갈아끼우기" 해결책이 템플릿 메소드 패턴. GoF 행위 패턴 중 하나, 상속 기반 가장 단순한 패턴.

이번 글에서 패턴 정의, 프론트엔드 관점 활용, 실무 적용 사례 정리.

---

## 2. 템플릿 메소드가 필요한 상황

데이터 fetch 로직 두 개. 공통 흐름 거의 동일.

```js
async function fetchUserList() {
showLoading();
try {
const res = await fetch('/api/users');
const data = await res.json();
const users = data.map(u => ({ ...u, name: u.name.trim() }));
renderUsers(users);
} catch (e) {
showError(e);
} finally {
hideLoading();
}
}

async function fetchProductList() {
showLoading();
try {
const res = await fetch('/api/products');
const data = await res.json();
const products = data.filter(p => p.active);
renderProducts(products);
} catch (e) {
showError(e);
} finally {
hideLoading();
}
}
```

문제점:

- 흐름(로딩 → fetch → 변환 → 렌더 → 에러 처리) 동일, 코드 중복
- 새 리스트 추가 시 같은 구조 또 작성
- 한 곳 수정 시 (예: 로딩 UI 변경) 전부 찾아 수정
- 흐름 누락 위험. finally 빠뜨리면 로딩 안 사라짐

핵심 흐름 한 곳 고정, 달라지는 부분만 분리 필요.

---

## 3. 템플릿 메소드란?

상위 클래스가 알고리즘 골격(template) 정의, 일부 단계를 하위 클래스가 오버라이드하는 패턴.

구성 요소:

- **템플릿 메소드**: 전체 흐름 정의. 변경 불가
- **추상 메소드(훅)**: 하위 클래스에서 반드시 구현
- **선택 메소드**: 기본 동작 있음, 필요 시 오버라이드

```js
class DataLoader {
// 템플릿 메소드 - 흐름 고정
async load() {
this.showLoading();
try {
const raw = await this.fetchData();
const transformed = this.transform(raw);
this.render(transformed);
} catch (e) {
this.handleError(e);
} finally {
this.hideLoading();
}
}

// 하위 클래스에서 구현해야 함
async fetchData() { throw new Error('구현 필요'); }
transform(data) { return data; } // 기본값 있음
render(data) { throw new Error('구현 필요'); }

// 공통 동작
showLoading() { /* ... */ }
hideLoading() { /* ... */ }
handleError(e) { console.error(e); }
}
```

사용:

```js
class UserLoader extends DataLoader {
async fetchData() {
const res = await fetch('/api/users');
return res.json();
}
transform(data) {
return data.map(u => ({ ...u, name: u.name.trim() }));
}
render(users) { renderUsers(users); }
}

new UserLoader().load();
```

핵심: `load()` 흐름은 절대 안 바뀜. 달라지는 건 `fetchData`, `transform`, `render`만.

---

## 4. 템플릿 메소드 활용 (프론트엔드 관점)

프론트엔드는 클래스 상속보다 함수 합성이 일반적. 하지만 템플릿 메소드 본질은 "흐름 고정 + 단계 주입". 상속 없이도 구현 가능.

### 함수형 버전

```js
async function createDataLoader({ fetchData, transform = (d) => d, render }) {
showLoading();
try {
const raw = await fetchData();
const data = transform(raw);
render(data);
} catch (e) {
handleError(e);
} finally {
hideLoading();
}
}

// 사용
createDataLoader({
fetchData: () => fetch('/api/users').then(r => r.json()),
transform: (data) => data.map(u => ({ ...u, name: u.name.trim() })),
render: renderUsers,
});
```

흐름 동일, 주입만 다름. 이게 프론트에서 자주 쓰는 형태.

### 친숙한 사례

이미 패턴 쓰고 있을 가능성 높음.

- **React 커스텀 훅**: `useQuery`가 로딩/에러/데이터 흐름 고정, `queryFn`만 주입
- **Axios interceptor**: 요청/응답 흐름 고정, 변환 로직 주입
- **Express 미들웨어**: 요청 처리 흐름 고정, 핸들러 주입

```js
// useQuery 내부 의사 코드
function useQuery({ queryKey, queryFn, select }) {
// 1. 캐시 확인 (고정)
// 2. 로딩 상태 (고정)
// 3. queryFn 실행 (주입)
// 4. select로 변환 (주입)
// 5. 에러 처리 (고정)
// 6. 캐시 저장 (고정)
}
```

TanStack Query 자체가 거대한 템플릿 메소드. 흐름은 라이브러리, 단계는 사용자 주입.

---

## 5. 템플릿 메소드 실무 적용

### 사례 1: 폼 제출 흐름 통일

폼마다 검증 → 제출 → 성공 처리 → 에러 처리 흐름 동일. 매번 작성하면 누락 발생.

```js
async function submitForm({ values, validate, submit, onSuccess, onError }) {
// 1. 검증
const errors = validate(values);
if (Object.keys(errors).length > 0) {
showValidationErrors(errors);
return;
}

// 2. 제출
setSubmitting(true);
try {
const result = await submit(values);
onSuccess(result);
showToast('저장 완료');
} catch (e) {
onError?.(e);
showToast('저장 실패', 'error');
} finally {
setSubmitting(false);
}
}
```

각 폼은 `validate`, `submit`, `onSuccess`만 정의. 토스트, 로딩, 에러 처리 누락 걱정 없음.

### 사례 2: 다단계 폼 진행 흐름

진료 처방전 같은 다단계 폼. 단계마다 검증 → 저장 → 다음 단계 이동 흐름 동일.

```js
class StepFormBase {
async goNext() {
if (!await this.validateStep()) return;
await this.saveStep();
this.moveToNext();
}

async validateStep() { throw new Error('구현 필요'); }
async saveStep() { throw new Error('구현 필요'); }
moveToNext() { /* 공통 라우팅 */ }
}
```

새 단계 추가 시 검증과 저장만 작성. 흐름 일관성 보장.

### 사례 3: API 클라이언트 래퍼

```js
async function apiCall({ url, method = 'GET', body, parser = (d) => d }) {
// 1. 토큰 주입 (고정)
const headers = { Authorization: `Bearer ${getToken()}` };

// 2. 요청 (고정)
const res = await fetch(url, { method, headers, body: JSON.stringify(body) });

// 3. 에러 처리 (고정)
if (!res.ok) throw new ApiError(res.status);

// 4. 파싱 (주입)
const data = await res.json();
return parser(data);
}
```

호출부는 url과 parser만 신경 씀. 토큰, 에러 처리 자동.

---

## 6. 정리

- 템플릿 메소드 = 흐름 고정 + 단계 주입
- 코드 중복 제거, 흐름 일관성 보장, 누락 방지
- 클래스 상속이 정석이지만, 프론트에서는 함수 + 콜백 주입이 더 자연스러움
- 이미 `useQuery`, `axios interceptor`, 폼 라이브러리에서 알게 모르게 쓰고 있는 패턴
- "이 로직 또 쓰고 있네"라는 느낌이 들면 템플릿 메소드 적용 신호

핵심은 "변하는 것과 변하지 않는 것 분리". 흐름은 고정, 세부는 주입. 이 한 줄이 전부.