일단 가드해야 할 타입들은 다음과 같습니다.
export enum TodoEnum {
DAILY = 'DAILY',
WEEKLY = 'WEEKLY',
MONTHLY = 'MONTHLY',
}
export type TodoDataBase =
| {
type: TodoEnum.DAILY
content: string
title: string
}
| {
type: TodoEnum.WEEKLY
total: Date
}
| {
type: TodoEnum.MONTHLY
goal: string
}
위 타입들을 파악해 보자면, 우선 TodoEnum
이라는 이넘 타입이 존재합니다. 즉, API 콜을 통해 받을 수 있는 Todo의 종류는 총 3가지로 이넘 타입을 이용해 선언해 두었네요.
그리고 이때 받게 되는 Todo의 종류마다 조금씩 다른 객체를 들고 옵니다. 그래서 위와 같이 TodoDataBase
라는 타입은 유니온 타입으로 이거일 수도 이거일 수도 이거일 수도라고 설명하고 있네요.
만약 한 컴포넌트에서 위와 같이 세 가지 Todo 종류를 처리해야 한다고 생각해 봅시다. 그렇다면 todo의 타입에 따라 조금씩 다른 View 로직을 작성해야겠죠?
우선 TodoEnum
을 이용해서 TodoDataBase
에서 유니온 중 하나를 가져오는 유틸 타입을 하나 선언해 봅시다.
📌 Util은 '유용한, 도움이 되는, 쓸모 있는'을 뜻하며...
지금 만들 유틸 타입은 TodoEnum
을 이용해서 TodoDataBase
에서 유니온 중 하나를 가져오는 타입입니다.
대강 구상을 해본다면, TodoEnum
을 제네릭으로 받아서 TodoDataBase
에서 Extract하면 되겠다 생각이 날 수 있습니다.
export type TodoType<T extends TodoEnum = TodoEnum> = Extract<
TodoDataBase,
{ type: T }
>
조금 설명을 보태보자면, 제네릭에 TodoEnum
중 하나를 받습니다. 예를 들면 TodoEnum.DAILY
정도가 되겠네요. 그럼 이때 TodoDataBase
에서 Extract합니다. 어떤 애를? 제네릭으로 받은 type
을 가진 애를!
추가로 기본적으로 TodoEnum 자체를 제네릭으로 넣어두어, 아무 것도 입력하지 않은 경우 TodoDataBase
전체를 가져올 수 있도록 합니다.
연습으로 아래와 같이 작성했다면 타입 A는 어떤 형태일까요?
type A = TodoType<TodoEnum.DAILY>
TodoEnum.DAILY
를 제네릭으로 넘겼더니 원하는대로 이에 해당하는 타입을 가져온 것을 확인할 수 있었습니다.
Type Alias
혹은 Interface
로 선언된 타입을 가드하기 위해서는 사용자 정의 타입 가드 함수를 사용할 수 있습니다.
그럼 아래와 같이 Todo 타입을 가드 함수를 만들 수 있습니다.
const todotypeIsDaily = (todo:TodoDataBase): todo is TodoType<TodoEnum.DAILY> => todo.type === TodoEnum.DAILY;
이제 이 booelan
값을 리턴하는 함수를 통해 todo
가 무슨 타입인지를 구별하고, 이에 맞는 View 로직을 return 하면 될 것 같습니다.
사실 타입 가드를 하는 방법에는 여러 방법이 존재합니다. 현재 TodoDataBase
를 살펴보면, 동일하게 type
이라는 프로퍼티가 존재해 이를 통해 가드를 하는 것도 사실 가능합니다. if todo.type === TodoEnum.DAILY
이런 식으로 말이죠.
import type { TodoDataBase } from '@/types/todo'
import { TodoEnum } from '../../types/todo'
interface Props {
todo: TodoDataBase
}
const OneTodo= = ({ todo }: Props) => {
if (todo.type === TodoEnum.DAILY) {
return (
<div>
<h3>{todo.title}</h3>
<div>{todo.content}</div>
<hr />
</div>
)
}
if (todo.type === TodoEnum.WEEKLY) {
return (
<div>
<h3>{JSON.stringify(todo.total)}</h3>
</div>
)
}
if (todo.type === TodoEnum.MONTHLY) {
return <div>{todo.goal}</div>
}
}
export default OneTodo
그런데, 이렇게 if문을 통해 View 로직을 return 하는 패턴은 유지보수에 좋지 못합니다. 중복되는 코드를 작성하게 되거나, 혹은 이벤트 함수가 추가될 경우 위 아래 코드를 넘나들며 확인해야 할 것입니다.
만약 컴포넌트를 분리한다고 하더라도, 해당 컴포넌트에서 중요한 로직이라고 할 수 있는 내용들 혹은 이벤트 함수들이 모두 감춰지게 되어 분리된 컴포넌트들을 넘나 들며 확인해야 할 수 있습니다.
물론 상황에 따라 다를 수 있지만요!
우선 즉시 실행 함수란, 선언과 동시에 실행하여 return 값 할당까지 즉시 가능합니다.
저는 아래와 같이 타입 가드 함수와 즉시 실행 함수를 이용했습니다.
const TODO = (() => {
if (todoTypeIsDaily(todo)) {
const { type, title, content } = todo
return { type, title, content }
}
if (todoTypeIsMonthly(todo)) {
const { type, goal } = todo
return { type, goal }
}
if (todoTypeIsWeekly(todo)) {
const { type, total } = todo
return { type, total }
}
})()
의문이 들 수 있는 점들에 대해 차근차근 정리해 봅시다.
우선 맨 처음에 사용했던 if todo.type === TodoEnum.DAILY
방식과는 다르게 사용자 정의 타입 가드를 선언해 사용했습니다.
1️⃣ 사용자 정의 타입 가드 함수를 사용할 경우 좀 더 안전하게 타입 검사가 가능해져 복잡하거나 다양한 조건 및 형태의 데이터를 다루어야 하는 경우 유용할 수 있습니다.
2️⃣ 반면 단순히 객체 속성을 비교해 가드하는 경우 간단하며 직관적인 비교 로직으로 필요한 기능을 충분히 구현할 수 있는 경우 사용하기 좋습니다.
우리의 경우라면 단순히 객체 속성을 비교해 가드해도 충분할 수 있지만, 만약 type이 동일하게 TodoEnum.DAILY
이고 나머지가 달라진다면요? 추가 조건문을 걸어 구분해 주어야 할 것이며, 이는 길어질 수록 가독성이 떨어지게 될 것입니다. 따라서 구체적인 데이터 형태를 다루기 위해서라면 사용자 정의 타입 가드 방법이 좀 더 안전해 보입니다.
위에서 만들었던 즉시 실행 함수가 실행되면 바로 todo의 타입을 체크하고 TODO
에는 바로 객체가 반환됩니다. 이렇게 되면, View 로직에서는 동시에 가능한 모든 여러 프로퍼티에 접근할 수 있게 됩니다.
이제 if 문에 따라 View 로직을 따로 따로 분리해 놓지 않아도 되었지만, 명확하게 todo의 타입에 따른 View 로직을 구분해 볼 수 없다는 점이 아쉽긴 합니다.
따라서 위와 같이 즉시 실행 함수를 사용하는 방법은 중복된 로직이 많고 타입의 종류에 따라서는 일부 UI의 추가가 필요한 상황에 좋을 것 같습니다.
결과적으로는, 아래처럼 View 로직을 작성했습니다.
return (
<>
<h2>{TODO?.type}</h2>
<hr />
<section>
{/* DAILY */}
<h3>{TODO?.title}</h3>
<p>{TODO?.content}</p>
{/* WEEKLY */}
<h3>{JSON.stringify(TODO?.total)}</h3>
{/* MONTHLY */}
<h3>{TODO?.goal}</h3>
</section>
</>
)
위에서 사용자 정의 타입 가드 함수 하나를 이렇게 작성했었습니다.
const todotypeIsDaily = (todo:TodoDataBase): todo is TodoType<TodoEnum.DAILY> => todo.type === TodoEnum.DAILY;
그런데 우리는 TodoEnum에 3가지가 있었죠? 그럼 아래와 같이 비슷하게 3번 써줘야 할 것입니다.
const todotypeIsDaily = (todo:TodoDataBase): todo is TodoType<TodoEnum.DAILY> => todo.type === TodoEnum.DAILY;
const todotypeIsWeekly = (todo:TodoDataBase): todo is TodoType<TodoEnum.WEEKLY> => todo.type === TodoEnum.WEEKLY;
const todotypeIsMonthly = (todo:TodoDataBase): todo is TodoType<TodoEnum.MONTHLY> => todo.type === TodoEnum.MONTHLY;
저는 이렇게 계속 비슷하지만 반복되는 로직을 보고 유틸로 만들어겠다고 생각이 들었는데요.
우선 위 타입 가드 함수들은 함수입니다. 그러므로 지금 만들 Util은 함수를 리턴하는 함수가 될 것입니다. 이때 함수를 리턴하는 함수를 고차 함수라고 부릅니다.
📌 고차 함수(Higher order function)
고차 함수는 함수를 인자로 전달받거나 함수를 결과로 반환하는 함수를 말한다.
아래와 같이 타입 가드 함수를 리턴하는 함수를 선언해 주었습니다.
export const todoTypeIs = <T extends TodoEnum>(type: T) => {
return (todo: TodoDataBase): todo is TodoType<T> => todo.type === type
}
복잡하네요. 좀 살펴봅시다. 일단 제네릭을 사용합니다. 인자로 TodoEnum을 extends 하고 있는 예를 들어 TodoEnum.DAILY
이런 식으로 넘겨 받게됩니다. 그러면 함수를 리턴하게 되어있죠? 이때 리턴되는 함수를 떼서 봅시다.
(todo: TodoDataBase): todo is TodoType<T> => todo.type === type
우리가 사용자 정의 타입 가드 함수를 선언했을 때 코드와 비슷합니다. 고차 함수의 인자로 받은 제네릭을 안에 중복되는 로직 중에 유일하게 달랐던 그 부분에 넣어주었을 뿐입니다.
사용하게 될 때는 아래와 같습니다.
const todoTypeIsDaily = todoTypeIs(TodoEnum.DAILY) //todoTypeIsDaily는 함수
...
if (todoTypeIsDaily(todo)) { ... }
이번 글에서는 Util 타입 혹은 Util 타입 가드 함수를 선언해 사용하는 방법 그리고 즉시 실행 함수를 활용하는 방법에 대해 알아보았습니다. 즉시 실행 함수를 사용함으로써 로직을 분리하지 않고 작성할 수 있었지만, 명확히 구분해 보지 못하게 되었다는 단점이 생기기도 했습니다. 따라서 상황에 따라 선택할 수 있어야겠습니다 :)
제가 이번 글을 남긴 이유는 이 방법이 좋은 방법이다! 라기보다는 이런 방법도 있구나 하고 정리해 보았던 것이였습니다 :)