복붙으로 늘어난 fetch 함수
카테고리: duplication · 이 패턴은 8기 PR 26건 중 12건에서 관찰됩니다.
popular/search 요청 함수를 통째로 복붙해 변경 지점을 늘린다.
getPopularMovies와 getSearchMovies가 URL 조립, fetch 옵션, ok 체크, 에러 throw까지 거의 동일한 코드를 반복합니다. 컨트롤러 단에서도 try-skeleton-fetch-render-catch 구조가 4~5곳에 그대로 복제되어, 헤더 한 줄만 바뀌어도 모든 사본을 찾아 고쳐야 합니다.
문제 코드
다음은 실제 8기 크루 PR에서 추출한 코드입니다. 작성자는 익명 처리하고 원본 PR 링크만 남깁니다.
사례 1
export async function getPopularMovies(pageNum) {
const url = `${API_PATH.POPULAR_MOVIE}?page=${pageNum}&language=ko-KR`;
const options = { method: "GET", headers: { Authorization: `Bearer ${API_KEY}` } };
const response = await fetch(url, options);
if (!response.ok) throw new Error('...');
return response.json();
}
export async function getSearchMovies(query, pageNum) { /* 동일 */ }두 함수가 options/fetch/ok 체크를 통째로 복사해 두어 변경 지점이 두 배가 됩니다. 원본: PR #277 ·
src/api.ts
사례 2
(async () => { /* topRated */ })();
(async () => {
renderSkeleton();
const movies = await errorTryCatch(
async () => await getMoviePopular({ page }),
(e) => { if (e.status_code == 22) { /* ... */ } alert("..."); }
);
})();alert 메시지와 status_code 22 분기가 4~5곳에 그대로 반복되어 에러 정책 변경이 일괄적이지 않습니다. 원본: PR #273 ·
src/main.ts
사례 3
export async function popularController() {
try {
movieListView.reset();
movieListView.skeletonRender(SKELETON_NUMBER);
const popularMovies = await getMovies(page);
// ...
} catch (error) { /* ... */ }
finally { movieListView.skeletonRemover(); }
}popular/search/morePopular/moreSearch 4개 컨트롤러가 동일한 try-catch-finally 구조를 복제합니다. 원본: PR #272 ·
src/controller/popularController.ts
스스로 진단해보기
해설을 펼치기 전에 다음 질문에 답한다.
- 두 함수의 차이점만 골라 매개변수로 추출했을 때 본문이 몇 줄이 남는지 센다.
- 공통 헬퍼로 묶었을 때 단점이 있다면 무엇인지 한 가지 적는다.
- 이 코드 변경 이력에서 같은 내용을 두 번 수정한 흔적을 찾는다.
해설
해설 보기
두 함수의 차이가 “URL 경로”와 “쿼리 파라미터” 두 가지뿐이라면, 나머지 90%는 한 곳에 있어야 합니다. 인증 헤더 한 줄을 바꾸려고 두 파일을 동시에 수정해야 한다면 이미 DRY 원칙 이 깨진 상태입니다.
추출의 실마리는 “달라지는 부분만 매개변수로 받는 헬퍼”입니다. fetch 옵션과 Response.ok 체크, JSON 파싱은 한 함수에 모으고, 경로와 쿼리만 호출자가 넘기게 합니다. URL 조립은 URLSearchParams와 URL 생성자로 안전하게 만들 수 있습니다.
컨트롤러 단의 try-skeleton-fetch-render-catch 반복도 같은 결을 가집니다. “스켈레톤을 켜고, 페칭하고, 렌더하고, 에러 시 표시하고, finally에서 정리한다”는 흐름은 한 군데 헬퍼에 두고 페칭 함수와 렌더 함수만 인자로 받게 만들면 4~5곳의 사본이 한 줄 호출로 줄어듭니다. 사본이 줄어들면 정책 변경 한 번에 한 곳만 고치면 됩니다.
개선 방향
Before
export async function getPopularMovies(page) {
const url = `${POPULAR}?page=${page}`;
const res = await fetch(url, { headers: AUTH });
if (!res.ok) throw new Error("...");
return res.json();
}
export async function getSearchMovies(query, page) {
const url = `${SEARCH}?query=${query}&page=${page}`;
const res = await fetch(url, { headers: AUTH });
if (!res.ok) throw new Error("...");
return res.json();
}After
async function tmdbGet<T>(path: string, params: Record<string, string | number>): Promise<T> {
const url = new URL(path, BASE);
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)));
const res = await fetch(url, { headers: AUTH });
if (!res.ok) throw new ApiError(res.status, await res.text());
return res.json();
}
export const getPopularMovies = (page: number) =>
tmdbGet(POPULAR, { page, language: "ko-KR" });
export const getSearchMovies = (query: string, page: number) =>
tmdbGet(SEARCH, { query, page, language: "ko-KR" });핵심 변화는 달라지는 부분만 매개변수로 추출하고 fetch·ok 체크·에러 변환을 한 헬퍼에 모아 변경 지점을 한 곳으로 줄였다는 점입니다.




