Javascript

TypeScript로 불필요한 변수 선언 없애기: `satisfies` 활용법

드리프트2 2024. 8. 24. 10:34

 

이번 글에서는 TypeScript에서 satisfies를 활용하여 코드 내 불필요한 변수 선언을 줄이는 방법을 소개합니다.

 

satisfies는 코드를 더 간결하고 효율적으로 만드는 데 유용한 기능입니다.

 

변수 선언 없이 타입을 지정하는 방법

타입 주석을 통해 변수 선언 시 타입을 지정할 수 있지만, 이로 인해 불필요한 변수를 선언해야 하는 경우가 많습니다.

 

satisfies를 사용하면 이러한 불필요한 변수를 줄일 수 있습니다. 구체적인 예시를 통해 알아보겠습니다.

 

예시 1: 타입 포괄성 체크

switch 문에서 모든 가능한 타입을 포괄적으로 확인하는 방법으로 never 타입을 활용할 수 있습니다.

 

다음은 그 예시입니다.

type Animal = "dog" | "cat" | "pig";

const awesomeFunction = (animal: Animal) => {
  switch (animal) {
    case "cat":
      return "meow";
    case "dog":
      return "bowwow";
    case "pig":
      return "oink oink";
    default:
      const _: never = animal;
  }
};

 

이 코드는 모든 case를 처리했기 때문에 default 절에 도달했을 때 animalnever 타입이 됩니다.

 

Animal에 새로운 값이 추가되면 컴파일러가 이를 감지하여 알려줍니다.

 

하지만 위 코드는 불필요한 변수를 선언하고 있습니다. satisfies를 이용해 이를 제거할 수 있습니다.

 

satisfies를 사용한 개선

satisfies를 사용하여 불필요한 변수를 제거한 코드는 다음과 같습니다.

type Animal = "dog" | "cat" | "pig";

const awesomeFunction = (animal: Animal) => {
  switch (animal) {
    case "cat":
      return "meow";
    case "dog":
      return "bowwow";
    case "pig":
      return "oink oink";
    default:
      return animal satisfies never;
  }
};

 

이렇게 하면 불필요한 변수 선언 없이도 필요한 기능을 구현할 수 있습니다.

 

default 절은 남은 animal이 없는지를 확인하는 역할을 하므로, 변수 선언이 필요하지 않습니다.

 

default 절에서 return animal을 하면 awesomeFunction의 타입은 다음과 같습니다.

typeof awesomeFunction = (animal: Animal) => "meow" | "bowwow" | "oink oink"

 

만약 return animal을 하지 않는다면 타입에 undefined가 포함됩니다. 따라서 return을 명시하는 것이 좋습니다.

 

예시 2: 함수 인자에 적용하기

라이브러리에서 종종 "인자로 객체를 받지만, 키는 문자열이면 상관없다"는 함수를 보게 됩니다.

 

이럴 때도 satisfies가 유용합니다. 예를 들어, zod를 사용한 폼을 구현해보겠습니다.

 

 

satisfies 없이 zod 스키마 선언

 

아래는 기본적인 폼 구현입니다.

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

const awesomeFormSchema = z.object({
  name: z.string(),
  address: z.string(),
});

type AwesomeFormSchema = z.infer<typeof awesomeFormSchema>;

const AwesomeFormPage = () => {
  const { register } = useForm<AwesomeFormSchema>({
    resolver: zodResolver(awesomeFormSchema),
  });

  return (
    <form>
      <label>
        이름
        <input {...register("name")} />
      </label>
      <label>
        주소
        <input {...register("address")} />
      </label>
    </form>
  );
};

 

여기서 "이름"과 "주소" 필드에 필수 입력과 오류 메시지를 추가해보겠습니다.

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

const awesomeFormSchema = z.object({
  name: z.string().min(1, { message: "이름은 필수 항목입니다." }),
  address: z.string().min(1, { message: "주소는 필수 항목입니다." }),
});

type AwesomeFormSchema = z.infer<typeof awesomeFormSchema>;

const AwesomeFormPage = () => {
  const {
    register,
    formState: { errors },
  } = useForm<AwesomeFormSchema>({
    resolver: zodResolver(awesomeFormSchema),
  });

  return (
    <form>
      <label>
        이름
        <input {...register("name")} />
        <p>{errors.name?.message}</p>
      </label>
      <label>
        주소
        <input {...register("address")} />
        <p>{errors.address?.message}</p>
      </label>
    </form>
  );
};

 

하지만 오류 메시지와 라벨에 사용된 문구가 각각 하드코딩되어 있는 점이 아쉽습니다.

 

이를 통일해보겠습니다.

const awesomeFormLabel = {
  name: "이름",
  address: "주소",
};

const awesomeFormSchema = z.object({
  name: z.string().min(1, { message: `${awesomeFormLabel.name}은 필수 항목입니다.` }),
  address: z.string().min(1, { message: `${awesomeFormLabel.address}은 필수 항목입니다.` }),
});

type AwesomeFormSchema = z.infer<typeof awesomeFormSchema>;

const AwesomeFormPage = () => {
  const {
    register,
    formState: { errors },
  } = useForm<AwesomeFormSchema>({
    resolver: zodResolver(awesomeFormSchema),
  });

  return (
    <form>
      <label>
        {awesomeFormLabel.name}
        <input {...register("name")} />
        <p>{errors.name?.message}</p>
      </label>
      <label>
        {awesomeFormLabel.address}
        <input {...register("address")} />
        <p>{errors.address?.message}</p>
      </label>
    </form>
  );
};

 

이렇게 하면 awesomeFormLabelawesomeFormSchema 사이에 타입적인 연결이 없기 때문에 다른 키를 추가할 수 있습니다.

 

이를 방지하기 위해 satisfies를 사용해봅시다.

 

satisfies로 개선하기

satisfies를 사용하면 불필요한 변수를 없애고 필요한 것을 구현할 수 있습니다.

import { z, type ZodSchema } from "zod";

type SchemaKey = "name" | "address";

const awesomeFormLabel = {
  name: "이름",
  address: "주소",
} as const satisfies Record<SchemaKey, string>;

const awesomeFormSchema = z.object({
  name: z.string().min(1, { message: `${awesomeFormLabel.name}은 필수 항목입니다.` }),
  address: z.string().min(1, { message: `${awesomeFormLabel.address}은 필수 항목입니다.` }),
} satisfies Record<SchemaKey, ZodSchema>);

 

이렇게 하면 불필요한 변수를 없애고 awesomeFormLabelawesomeFormSchema 간에 타입적 연결을 설정할 수 있습니다.

satisfies를 사용하면 변수를 선언하지 않고도 타입을 강제할 수 있습니다.

 

이처럼 satisfies는 코드를 간결하게 하고 타입 안전성을 높이는 데 유용합니다.

 

요약

이번 글에서는 satisfies를 활용하여 불필요한 변수 선언을 줄이고 타입 안전성을 높이는 방법을 알아보았습니다.

 

다양한 상황에서 satisfies가 유용하게 쓰일 수 있으니, 아직 사용해보지 않으셨다면 꼭 시도해보시기 바랍니다!

 

그럼.