렌더와 fetch가 뒤섞인 함수
카테고리: responsibility · 이 패턴은 8기 PR 26건 중 7건에서 관찰됩니다.
render와 fetch를 한 함수에 섞어 이름과 책임이 어긋난다.
renderMovies, renderFetchMovieItem, getPopularMovies 같은 이름의 함수가 fetch와 DOM 렌더, 에러 표시, total_pages 반환까지 함께 처리하는 패턴입니다. 함수명이 약속한 책임과 실제 동작이 어긋나서 호출자 입장에서 부수효과를 예측할 수 없습니다.
문제 코드
다음은 실제 8기 크루 PR에서 추출한 코드입니다. 작성자는 익명 처리하고 원본 PR 링크만 남깁니다.
사례 1
export const renderMovies = async (moviePageCount: number) => {
try {
const movieData: MovieResponse = await fetchMovies(moviePageCount);
if (moviePageCount === 1) { renderBanner(movieData.results[0]); }
const list = document.querySelector(".thumbnail-list");
movieData.results.forEach(/* ... */);
return movieData.total_pages;
} catch { alert("..."); return 0; }
};renderer 이름인데 fetch, 에러 alert, total_pages 반환까지 모두 떠맡고 있어 재사용이 어렵습니다. 원본: PR #270 ·
src/movieRenderer.ts
사례 2
export async function getPopularMovies(arg: {
pageNum: number;
onSuccess: (data) => void;
onError: (error) => void;
onLoading: () => void;
}) { /* ... */ }API 함수가 onSuccess/onError/onLoading 콜백을 받아 UI 전이까지 직접 일으키므로 순수한 fetch가 아닙니다. 원본: PR #278 ·
src/api.ts
사례 3
export const renderFetchMovieItem = async ($target, page, query) => {
try {
showSkeleton($target);
const data = query ? await fetchSearchMovies(query, page) : await fetchPopularMovies(page);
renderResult($target, data, page, query);
return data.total_pages;
} catch (error) {
removeSkeleton($target);
showMoreButton();
throw new Error('영화 목록을 불러오지 못했습니다!');
}
};함수 이름 자체에 fetch와 render 두 책임이 같이 들어 있어, 호출자가 어디까지 일어나는지 예측하기 어렵습니다. 원본: PR #286 ·
src/render.ts
스스로 진단해보기
해설을 펼치기 전에 다음 질문에 답한다.
- 이 함수의 이름이 약속하는 일과 실제 부수효과를 나란히 적어 차이를 본다.
- fetch와 render를 분리했을 때 호출 코드가 어떻게 바뀌는지 손으로 그려 본다.
- 이 함수를 테스트한다면 어떤 mock이 필요한지 나열한다.
해설
해설 보기
함수 이름은 일종의 계약입니다. render라고 적었으면 그 함수는 화면을 그리는 일만 책임져야 하고, get이라고 적었으면 데이터를 가져와 반환하는 일만 해야 합니다. 이 계약이 깨지면 호출자는 함수 이름을 더 이상 신뢰할 수 없게 되고, 모든 호출 지점이 본문을 다시 읽어야 부수효과를 파악할 수 있게 됩니다. 이른바 최소 놀람의 원칙 이 깨진 상태입니다.
이 패턴이 만드는 가장 큰 손해는 테스트 가능성입니다. renderMovies가 fetch와 DOM 조작, alert을 모두 가지면, 렌더 로직만 검증하려 해도 네트워크와 window 객체를 모두 mock해야 합니다. 반대로 fetch는 데이터만 반환하고 render는 데이터만 받아서 그리는 순수 함수 에 가까워지면, 각 단계는 입력과 출력만으로 검증할 수 있습니다.
또 한 가지는 재사용성입니다. fetch와 render가 묶여 있으면 “데이터만 가져와서 다른 곳에 쓰기”가 불가능합니다. 페이지를 미리 캐시하거나, 동일한 데이터를 다른 뷰에 그리거나, 서버에서 받은 데이터를 가공해서 표시하는 변형이 모두 막힙니다. 두 책임을 떼어 두면 조합의 폭이 즉시 넓어집니다.
개선 방향
Before
export const renderMovies = async (page: number) => {
try {
const data = await fetchMovies(page);
list.replaceChildren();
return data.total_pages;
} catch {
alert("실패");
return 0;
}
};After
// 1) 데이터만 가져온다
export const fetchMovies = (page: number): Promise<MovieResponse> =>
tmdbGet("/movie/popular", { page });
// 2) 받은 데이터로 그리기만 한다
export const renderMovies = (movies: Movie[], target: HTMLElement) => {
target.replaceChildren(...movies.map(toMovieElement));
};
// 3) 호출자가 둘을 조합한다
const data = await fetchMovies(page);
renderMovies(data.results, listElement);핵심 변화는 함수 이름이 약속하는 한 가지 일만 하도록 fetch와 render를 떼고, 호출자가 둘을 조합하도록 책임을 옮겼다는 점입니다.




