※ 아이템28 유효한 상태만 표현하는 타입을 지향하기
웹애플리케이션을 만들 때 페이지의 상태관리 설계
interface State {
pageText: string
isLoading: boolean
error?: string
}
declare let currentPage: string
function renderPage(state: State) {
if (state.error) {
return `Error! Unable to load ${currentPage}: ${state.error}`
} else if (state.isLoading) {
return `Loading ${currentPage}...`
}
return `<h1>${currentPage}</h1>\n${state.pageText}`
}
페이지를 그리는 renderPage 함수를 작성할 때는 상태 객체의 필드를 고려해서 상태 표기를 분기해야 한다.
그런데 분기조건이 명확히 분리되어있지 않다. isLoading 이 true 이고 동시에 error 값이 존재하면 ? -> 오류가 발생한 상태인지 로딩중인 상태인지 명확하게 구분할 수 없다.
interface State {
pageText: string
isLoading: boolean
error?: string
}
declare let currentPage: string
function getUrlForPage(p: string) {
return ''
}
async function changePage(state: State, newPage: string) {
state.isLoading = true
try {
const response = await fetch(getUrlForPage(newPage))
if (!response.ok) {
throw new Error(`Unable to load ${newPage}: ${response.statusText}`)
}
const text = await response.text()
state.isLoading = false
state.pageText = text
} catch (e) {
state.error = '' + e
}
}
isLoading 은 우선 true 로 고정한다.
response 값이 ok 가 아니라면 예외를 던지고, ok 라면 다시 await 로 text() 를 받아온다.
isLoading 을 false 로 바꿔주고, pageText 는 text 로 바꾼다.
그리고 예외로 들어왔을 때는 error 를 문자로 변환하여 리턴한다.
이 changePage 에도 문제점이 있다.
- 오류가 발생했을 때 isLoading 을 false 로 설정하는 로직이 빠져있다.
- error 를 초기화하지 않았기 때문에, 페이지 전환중에 사용자는 로딩메세지 대신 과거의 오류 메세지를 보여주게 된다.
- 페이지 로딩중에 사용자가 페이지를 바꿔버리면
- 새 페이지에 오류가 뜨거나
- 응답이 오는 순서에 따라 두번째 페이지가 아닌 첫번째 페이지로 전환될 수 있다.
- 상태값의 두가지 속성이 동시에 정보가 부족하거나, 두가지 속성이 충돌할 수 있다. State 타입은 isLoading 이 true 이면서 동시에 error 값이 설정되는 상태를 허용하고 있기 때문이다.
무효한 상태가 존재해서는 안된다. 코드를 다음과 같이 변경해보자
interface RequestPending {
state: 'pending'
}
interface RequestError {
state: 'error'
error: string
}
interface RequestSuccess {
state: 'ok'
pageText: string
}
type RequestState = RequestPending | RequestError | RequestSuccess
interface State {
currentPage: string
requests: { [page: string]: RequestState }
}
RequestPending, RequestError, RequestSuccess 세가지 인터페이스를 만들어놓고, 이 타입들로 RequestState 라는 유니온 타입을 만든다. 그리고 인터페이스 State 에서는 상태가 Pending인지, Error인지, Success인지 3가지 중 하나만 들어올 수 있도록 설계했다.
위 코드에서는 각각의 상태를 명시적으로 알려주는 태그된 유니온이 사용되었다.
또한 페이지 내에서 발생하는 모든 요청의 상태를 포함해 모델링 되었다. 이것을 이용해 renderPage, changePage 함수를 다시 구현해보자.
function renderPage(state: State) {
const { currentPage } = state
const requestState = state.requests[currentPage]
switch (requestState.state) {
case 'pending':
return `Loading ${currentPage}...`
case 'error':
return `Error! Unable to load ${currentPage}: ${requestState.error}`
case 'ok':
return `<h1>${currentPage}</h1>\n${requestState.pageText}`
}
}
async function changePage(state: State, newPage: string) {
state.requests[newPage] = { state: 'pending' }
state.currentPage = newPage
try {
const response = await fetch(getUrlForPage(newPage))
if (!response.ok) {
throw new Error(`Unable to load ${newPage}: ${response.statusText}`)
}
const pageText = await response.text()
state.requests[newPage] = { state: 'ok', pageText }
} catch (e) {
state.requests[newPage] = { state: 'error', error: '' + e }
}
}
requests 에서 currentPage 해당하는 값을 받아서 requestState 변수에 할당한다.
requestState 의 state 속성이 어떤 값이냐에 따라서 case를 나눌 수 있게 되었다.
그리고 changePage 에서는
request의 [newPage] 값에 상태와 페이지에 필요한 데이터들을 넣어준다 -> 페이지 별로 에러 구문이 따로 들어가게 됨
코드를 수정하며 이전코드에서 renderPage,changePage 의 모호함이 사라졌다.
모든 요청은 하나의 상태로 맞아 떨어진다 -> 요청이 진행중인 상태에서 사용자가 페이지를 변경하더라도 문제가 없다.
요약
- 유효한 상태와 무효한 상태를 둘 다 표현하는 타입은 혼란을 초래하기 쉽고 오류를 유발하게 된다
- 유효한 상태만 표현하는 타입을 지향해야 한다. 코드가 길어지거나 표현하기 어렵지만 결국은 시간을 절야가고 고통을 줄일 수 있다
'Typescript' 카테고리의 다른 글
[Typescript] 이펙티브 타입스크립트 3 아이템23-25 (0) | 2023.03.15 |
---|---|
[Typescript] 이펙티브 타입스크립트 3 아이템21-22 (0) | 2023.03.13 |
[Typescript] 이펙티브 타입스크립트 3 아이템19-20 (0) | 2023.03.09 |
[Typescript] 이펙티브 타입스크립트 2장 아이템17-18 (0) | 2023.03.07 |
[Typescript] 이펙티브 타입스크립트 2장 아이템15-16 (0) | 2023.03.07 |