Skip to Content
영화 리뷰 미션여러 곳에 흩어진 상태의 출처

여러 곳에 흩어진 상태의 출처

카테고리: state · 이 패턴은 8기 PR 26건 중 6건에서 관찰됩니다.

DOM, URL, 플래그, 파생값을 여러 곳에 중복 저장한 뒤 손으로 동기화한다.

totalPages를 렌더 함수의 반환값으로 받아 다시 state에 저장하거나, 헤더 hidden 속성과 classList를 따로 토글하는 패턴입니다. 진실의 출처가 여러 곳이라 한쪽만 갱신되면 즉시 화면이 어긋나는 파생 상태 동기화 누락이 자주 발생합니다.

문제 코드

다음은 실제 8기 크루 PR에서 추출한 코드입니다. 작성자는 익명 처리하고 원본 PR 링크만 남깁니다.

사례 1

if (!this.state.isSearched) { this.state.moviePageCount += 1; const totalPopularPages = await renderMovies(this.state.moviePageCount); if (totalPopularPages === this.state.moviePageCount) { this.hideLoadButton(); } }

렌더 함수가 total을 리턴하고 다시 state에 저장하는 구조라 파생값 동기화를 빠뜨리기 쉽습니다. 원본: PR #270  · src/App.ts

사례 2

const syncHeroSection = (elements: AppElements) => { const shouldShowHero = movieListStore.query === "" && movieListStore.movies.length > 0; elements.heroSection.hidden = !shouldShowHero; elements.siteHeader.classList.toggle("site-header--overlay", shouldShowHero); renderHeroMovie(movieListStore.movies[0], elements); };

헤더 오버레이 상태가 hidden 속성과 classList 두 군데로 분리되어 store 변경 시 수동으로 맞춰 줘야 합니다. 원본: PR #281  · src/main.ts

사례 3

params.set("keyword", keyword); params.set("page", String(1)); url.search = params.toString(); window.history.pushState({}, "", url.toString()); thumbnailListElement.textContent = "";

페이지 상태가 URL 쿼리와 DOM 초기화에 분산되어, 두 곳 중 하나만 갱신되면 즉시 어긋납니다. 원본: PR #280  · src/dom/eventHandler/handleMovieSearch.ts

스스로 진단해보기

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

  1. 이 화면 요소의 진짜 진실의 출처가 어느 변수인지 한 줄로 적는다.
  2. 파생값을 함수로 매번 계산했을 때 성능 문제가 진짜 있는지 추정한다.
  3. 한쪽만 갱신되었을 때 어떤 버그가 발생하는지 한 가지 시나리오를 적는다.

해설

해설 보기

진실의 출처(Single Source of Truth) 는 “어떤 값에 대한 정답이 어디에 있는가”에 대한 합의입니다. 같은 값이 두 곳에 저장되면 두 값이 같다는 것을 코드가 보장해야 하고, 그 보장이 깨지는 순간 화면은 거짓말을 하기 시작합니다.

이 패턴의 함정은 보통 파생값을 저장한다는 데서 출발합니다. total_pages는 응답 객체에 이미 존재하므로 별도로 저장할 필요가 없고, “현재 페이지가 마지막인지”는 page === total이라는 한 줄의 함수로 계산할 수 있습니다. 저장 대신 getter 나 함수로 표현하면 동기화를 빼먹을 일 자체가 없어집니다.

DOM도 마찬가지입니다. hidden 속성과 classList를 별도로 토글하기보다, 상태 객체 하나를 진실의 출처로 두고 그 변화를 구독해 한 곳에서 DOM에 반영 하는 편이 안전합니다. URL과 메모리 상태가 둘 다 필요하다면 한쪽을 진실로 정하고 다른 쪽은 항상 그것을 미러링하는 단방향 흐름으로 정리합니다.

개선 방향

Before

const total = await renderMovies(page); this.state.total = total; if (this.state.page === this.state.total) hideButton();

After

const movies = await fetchMovies(state.page); state.movies = movies; const isLastPage = () => state.page >= movies.total_pages; moreButton.toggle(!isLastPage());

핵심 변화는 파생값을 저장하지 않고 매번 계산하도록 바꿔서 동기화 자체가 필요 없게 만들었다는 점입니다.

더 알아볼 개념

Last updated on