Skip to Content
영화 리뷰 미션렌더와 fetch가 뒤섞인 함수

렌더와 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

스스로 진단해보기

해설을 펼치기 전에 다음 질문에 답한다.

  1. 이 함수의 이름이 약속하는 일과 실제 부수효과를 나란히 적어 차이를 본다.
  2. fetch와 render를 분리했을 때 호출 코드가 어떻게 바뀌는지 손으로 그려 본다.
  3. 이 함수를 테스트한다면 어떤 mock이 필요한지 나열한다.

해설

해설 보기

함수 이름은 일종의 계약입니다. render라고 적었으면 그 함수는 화면을 그리는 일만 책임져야 하고, get이라고 적었으면 데이터를 가져와 반환하는 일만 해야 합니다. 이 계약이 깨지면 호출자는 함수 이름을 더 이상 신뢰할 수 없게 되고, 모든 호출 지점이 본문을 다시 읽어야 부수효과를 파악할 수 있게 됩니다. 이른바 최소 놀람의 원칙 이 깨진 상태입니다.

이 패턴이 만드는 가장 큰 손해는 테스트 가능성입니다. renderMoviesfetch와 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를 떼고, 호출자가 둘을 조합하도록 책임을 옮겼다는 점입니다.

더 알아볼 개념

Last updated on