diff --git "a/doeun/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/doeun/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..cb3e057 --- /dev/null +++ "b/doeun/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,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`, 폼 라이브러리에서 알게 모르게 쓰고 있는 패턴 +- "이 로직 또 쓰고 있네"라는 느낌이 들면 템플릿 메소드 적용 신호 + +핵심은 "변하는 것과 변하지 않는 것 분리". 흐름은 고정, 세부는 주입. 이 한 줄이 전부.