3 / 3

상태 전이에 스프레드를 쓰지 않는다

타입이 막아주는 건 "없어야 할 걸 쓰는 것"이지, "남아 있는 걸 끌고 오는 것"이 아니다.

유니언으로 바꾼 뒤

팀은 안심했다. 불가능한 상태는 이제 없다.

상태를 바꾸는 코드는 여전히 손에 익은 대로 썼다. 기존 값은 유지하고, 바꿀 것만 덮어쓴다. 짧고, 읽기 쉽고, 타입 체크도 통과했다.

useProfile.ts
setState({ ...state, status: "error", error: e });

몇 주 뒤

에러 리포팅 도구를 붙이기로 했다. "에러 상태가 되면 state를 통째로 보내자. 디버깅하기 편하게."

같은 주에 다른 사람이 헤더를 고쳤다. 데이터가 있으면 사용자 이름을 보여준다.

errorReporting.ts
if (state.status === "error") {
  report(state);
}
Header.tsx
if ("data" in state) {
  showUserName(state.data.name);
}

그리고 어느 날

로그를 보던 사람이 이상한 걸 발견했다. 에러 로그마다 직전 사용자의 개인정보가 통째로 붙어 있었다.

같은 날, 계정 전환 중에 에러가 난 사용자의 헤더에는 이전 계정의 이름이 떠 있었다.

실제로 만들어진 상태
{
  status: "error",
  error: e,
  data: 이전 User,
}

왜 이렇게 됐나

스프레드는 이전 상태의 필드를 전부 복사해온다. 그리고 TypeScript는 스프레드로 들어온 여분 필드를 검사하지 않는다.

유니언이 막은 건 "error 상태에 data를 쓰는 것"이었다. 이미 있던 data가 따라오는 것까지는 막지 못했다.

터지기 전에

상태마다 전이 함수를 두고, 그 함수로만 바꾼다. 함수는 필요한 필드만으로 새 객체를 만들기 때문에 끌려올 게 없다.

"기존 거 유지하고 이것만"은 AI가 가장 자주 쓰는 손버릇이기도 하다. 리뷰로 매번 잡는 것보다, 쓸 길을 없애는 편이 싸다.

profileState.ts
const toLoading = (): State => ({ status: "loading" });
const toSuccess = (data: User): State => ({ status: "success", data });
const toError = (error: Error): State => ({ status: "error", error });
 
setState(toError(e));