모든 일을 떠맡은 main 함수
카테고리: responsibility · 이 패턴은 8기 PR 26건 중 16건에서 관찰됩니다.
main.ts/App가 fetch, 렌더, 상태, 이벤트를 한 메서드에 모두 담는다.
최상위 load 핸들러나 App 클래스의 한 메서드가 데이터 페칭, DOM 쿼리, 렌더, 스켈레톤, 라우팅, 에러 UX, 이벤트 바인딩까지 한꺼번에 처리하는 패턴입니다. 50~150줄 규모의 God handler가 만들어지고, 변경 한 가지에 영향 범위가 폭발적으로 늘어납니다.
문제 코드
다음은 실제 8기 크루 PR에서 추출한 코드입니다. 작성자는 익명 처리하고 원본 PR 링크만 남깁니다.
사례 1
private handleSearchSubmit = async () => {
this.state.isSearched = true;
this.state.searchPageCount = 1;
// ...
const list = document.querySelector(".thumbnail-list");
if (list) list.replaceChildren();
const header = document.querySelector<HTMLElement>("#header");
if (header) { header.replaceChildren(); replaceBanner(header); }
this.state.totalSearchPages = await renderSearchedMovies();
};한 핸들러에서 상태 토글, DOM 초기화, 배너 교체, 페칭, 페이지 카운트 수정까지 한꺼번에 합니다. 원본: PR #270 ·
src/App.ts
사례 2
const loadPopularMovies = async () => {
try {
renderSkeleton();
const page = pageState.getPage();
const movies = await getPopularMovies({ page });
if (movies) { renderMovieList(movies); updateMoreButton(movies.page, movies.total_pages); }
} catch (e) { showErrorAlert(e); }
finally { removeSkeleton(); }
};main.ts 한 파일이 상태, 페칭, 렌더, 스켈레톤, 라우팅, 이벤트 바인딩, 에러 UI를 모두 가집니다. 원본: PR #271 ·
src/main.ts
사례 3
#searchEventHandler = async () => {
const searchValue = this.#views.search.getInputValue();
this.#views.topRated.hide();
this.#views.movieList.hideNotFound();
this.#state.searchMoviePage = 1;
window.scrollTo({ top: 0, behavior: "instant" });
this.#views.movieList.renderTitle(`\"${searchValue}\"검색 결과`);
try { /* fetchSearchedMovies() */ }
catch (error) { alert(ERROR_MESSAGE.MOVIE.FAILED_SEARCH); }
finally { this.#views.movieList.removeAllSkeletons(); }
};View 6개 제어, 상태 5곳 수정, 스크롤, 페칭, 에러를 한 핸들러가 모두 책임집니다. 원본: PR #283 ·
src/App.ts
스스로 진단해보기
해설을 펼치기 전에 다음 질문에 답한다.
- 이 함수가 책임지고 있는 일을 동사로 모두 적어 본다.
- 각 동사 중 어떤 것을 별도 함수나 모듈로 옮기면 테스트가 가능해지는지 판단한다.
- 이 함수가 변경되어야 하는 이유를 세 가지 이상 떠올린다.
해설
해설 보기
함수의 책임은 그 함수가 “변경되어야 하는 이유”의 수와 같습니다. 페칭, 렌더, 상태, 이벤트, 에러 UX, 라우팅이 모두 한 함수에 들어 있다면 “변경되어야 하는 이유”가 최소 여섯 가지입니다. 이 원칙이 이른바 단일 책임 원칙 의 출발점입니다.
영향은 두 방향에서 나타납니다. 첫째, 테스트가 불가능해집니다. 한 함수 안에 fetch, DOM 조작, 상태 변경, alert이 섞여 있으면 입력을 주고 출력을 검증하는 단위 테스트를 만들 수 있는 경계가 없습니다. 둘째, 재사용이 불가능해집니다. 같은 페칭 로직이 필요한 다른 화면이 생기면 함수 전체를 복붙하게 되고, 이는 곧 ap-08의 중복 패턴으로 이어집니다.
분리의 출발점은 “데이터를 가져오는 일”과 “데이터를 그리는 일”을 다른 모듈로 떼는 것입니다. 그 사이에 상태 객체나 이벤트 디스패치 를 두고, 핸들러는 “어떤 액션을 일으킬지” 결정만 하도록 줄입니다. 각 단계가 하나의 동사만 책임지면 함수 이름이 그 동사 그대로가 되어 가독성도 함께 올라갑니다.
개선 방향
Before
const handleSearch = async () => {
state.isSearched = true;
state.searchPage = 1;
document.querySelector(".thumbnail-list")?.replaceChildren();
document.querySelector("#header")?.replaceChildren();
const movies = await fetchSearchMovies(query, 1);
renderList(movies);
if (!movies) alert("검색 실패");
};After
// 1) 핸들러는 상태 전이만 일으킨다
const handleSearchSubmit = (query: string) => {
searchStore.start(query);
};
// 2) 상태 변경을 구독해 데이터를 가져온다
searchStore.subscribe(async ({ query, page }) => {
try {
const movies = await searchMovies(query, page);
movieListView.render(movies);
} catch (error) {
errorView.show(error);
}
});핵심 변화는 핸들러는 상태 전이만 일으키고, 페칭과 렌더는 상태 변경에 반응하는 별도 모듈로 떼어내 책임을 한 가지씩 가지게 했다는 점입니다.




