[타입설계 28] 타입엔 유효한 상태를 담아라

2022. 7. 25. 19:56책/이펙티브 타입스크립트

효과적 타입 설계를 위해선 유효한 상태만 표현할 수 있는 타입을 만들어 내는 것이 가장 중요하다. 즉, 타입 설계시 어떤 값들을 포함하고 어떤 값들을 제외할지 신중해야 한다는 뜻이다.

물론 유효한 상태만 표현하기 위해 코드가 길어지거나 표현이 어려워질 수도 있다. 그러나 결국엔 시간을 절약하고 고통을 줄이는 방향이 될 것이다.

1. 유효한 상태
명확히 정의내리기 어렵지만 어떤 속성들에 대하여 정보가 부족하거나 충돌하지 않는 상태라고 생각할 수 있다. 아래 예시를 통해 유효하지 않은 상태를 보자.

가. 렌더링 상황(예시)

  • 분기 조건이 명확히 분리되어 있지 않다
  • isLoading이 true인데 동시에 error가 존재하면? 로딩중인거여 에러가 났다는거여?
  • 현 상태는 두 가지 속성이 충돌할 수 있음을 내포한다
interface State {
  pageText :string;
  isLoading :boolean;
  error? :string;
}

function renderPage(state :State) {
  if (state.error) {
    return `에러발생`;
  } else if (state.isLoading) {
    return `로딩중`;
  }
  return `<h1>${state.pageText}</h1>`;
}


나. 비동기 요청 상황

아래 코드 또한 얼핏보면 별 문제가 없지만, 사실 많은 문제점을 내포하고 있다

  • 오류가 발생했을 때 state.isLoading을 false로 바꿔줘야 하는데 빠져있다
    = 요청이 실패했다는 건지 로딩중인 건지 알 수가 없다
    = 속성들에 대한 정보가 부족하다
  • state.error를 초기화해두지 않아서 이전에 에러내용이 있었다면 엄한 오류메시지를 보여주게 된다
  • 페이지 로딩 중에 유저가 페이지를 바꿔버리면 어떤 일이 벌어질지 예상하기 어렵다. 비동기 요청이 중단되는건지 새 페이지에 오류가 뜨는건지 뭐가 어떻게 되는건지 알 길이 없다.
async function changePage(state :State, newPage :string) {
  // 로딩중 상태로 변경한다
  state.isLoading = true;

  try {
    const response = await fetch(`${newPage}로 요청하는 무언가`);
    if (!response.ok) {
      throw new Error(`${newPage}는 이용할 수 없는데요?`)
    }
    const text = await response.text();

    // 로딩이 끝났음을 알리고 페이지텍스트를 담는다
    state.isLoading = false;
    state.pageText = text;
  } catch(err) {
    state.error = err
  }
}

 

2. 코드 개선

위 예시들의 유효하지 않은 상태를 세 부분으로 나눠 뜯어고쳐보자.

 

가. 타입 선언

// 태그된 유니온을 위해 각 인터페이스마다
// 동일한 이름의 state 프로퍼티키를 추가했다
interface ReqPending {
  state :'pending';
}

interface ReqError {
  state :'error';
  error :string;
}

interface ReqSuccess {
  state :'success';
  pageText :string;
}

// 비록 코드 길이가 길어지기는 했지만
// 유효하지 않은 상태를 방지할 수 있다
type RequestState = ReqPending | ReqError | ReqSuccess;
interface State {
  currPage :string;
  requests : {
    [page :string] :RequestState
  };
}

 

나. 렌더링

  • 분기 조건이 명확해졌다(pending | error | success중 하나로만 결정된다)
  • 이에 따라 상태간의 충돌 여지도 사라졌다
function renderPage(state :State) {
  const { currPage } = state;
  const requestState = state.requests[currPage];

  switch (requestState.state) {
    case 'pending':
      return `로딩중임 ㄱㄷ`;
    case 'error':
      return `에러났따 ㅈㅅ;;`;
    case 'success':
      return `<h1>${requestState.pageText}</h1>`
  }
}

 

다. 비동기 요청

  • 모든 비동기 요청은 정확히 하나의 상태로 맞아 떨어진다
  • 요청이 진행 중인 상태에서 유저가 페이지를 변경한다고 한들, UI에 영향을 미치진 않는다(요청은 지속해서 실행되고 있을 것이며, 바뀐 페이지에서 오류를 표출한다던가 그럴 일이 없다는 뜻)
async function changePage(state :State, newPage :string) {
  state.requests[newPage] = { state: 'pending' };
  state.currPage = newPage;
  
  try {
    const response = await fetch(`${newPage}를 요청하는 무언가의 URL`);
    if (!response.ok) {
      throw new Error(`에러남 : ${newPage}`);
    }
    const pageText = await response.text();
    state.requests[newPage] = { state: 'success', pageText };
  } catch(err) {
    state.requests[newPage] = { state: 'error', error: err }
  }
}