Typescript 제네릭 실제 사용하기

 

커스텀 훅으로 만든 기존에 있던 useForm이다.

훅을 사용하는 두 가지 폼이 있는데 login과 todo를 생성, 수정할때 사용했다.


따라서 들어오는 타입도 TodoType, LoginType을 따로 만들어 union type으로 생성했는데 이렇게 사용하게 되니 계속 as로 Type Assertion(타입 단언) 시켜줘야하는 문제가 발생했다.

 

뿐만 아니라 자동완성기능도 사용하지 못했고 나중에 새로운 데이터 타입으로 사용할 때는 새로운 타입으로 useForm 자체에 추가해줘야하는 번거로움까지 발생했다.

 

이런 부분이 명백히 문제라고 생각했고 이번에 프리온보딩을 들으면서 적절히 추상화되지 않았다가 정답인거 같아 타입스크립트에 대해 조금 더 찾아보았다.

import { AxiosResponse } from "axios";
import React, { useEffect } from "react";
import { ErrorProps, SignType } from "../types/form";
import { TodoType } from "../types/todo";
import { ValueType } from "../types/util";

interface SubmitType {
  API: (value: ValueType) => Promise<AxiosResponse<any, any>>;
  onSuccess: (value: any) => void;
}
interface FormProps {
  initialValue: ValueType;
  validate?: (initialValue: SignType) => ErrorProps;
  onSubmit: () => SubmitType;
}

const useForm = ({ initialValue, validate, onSubmit }: FormProps) => {
  const [values, setValues] = React.useState(initialValue);
  ...

  return {
    values,
    handleChange,
    handleSubmit,
    error,
  };
};

export default useForm;

 

제네릭 적용하기

아래 문서를 참고했으며 재사용성이 높고 여러 가지 타입에서 동작하는 컴포넌트를 생성하는데 사용된다는 설명에 따라 조금 더 고민해봤다. 우선 들어와야하는 타입들은 이미 object 타입으로 만들어진 data가 있어 해당 객체를 타입으로 만들고 그 타입을 통해 선언한다면 조금 더 나은 구조가 될 수 있을거라고 생각했다

 

제네릭 | 타입스크립트 핸드북

제네릭(Generics)의 사전적 정의 제네릭은 C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징입니다. 특히, 한가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를

joshua1988.github.io

 

그래서 만들어진 코드는 다음과 같다. 제네릭 타입으로 선언하면서 새로 form을 사용할 때마다 타입을 수동으로 설정해줘야하는 번거로움과 as를 사용하는 부분이 많이 줄었다

import React, { useEffect } from "react";
import { ErrorProps } from "../types/form";

interface SubmitType<Type> {
  API: (value: Type) => Promise<unknown>;
  onSuccess: (value: unknown) => void;
}
interface FormProps<Type> {
  initialValue: Type;
  validate?: (initialValue: Type) => ErrorProps;
  onSubmit: () => SubmitType<Type>;
}

function useForm<T>({ initialValue, validate, onSubmit }: FormProps<T>) {
  const [values, setValues] = React.useState(initialValue);
  ...
  
  return {
    values,
    handleChange,
    handleSubmit,
    error,
  };
}

export default useForm;

 

Before

  const LoginAPI = (value: ValueType) => API.signin(value as SignType);
  const LoginLogic = useLogin();

After

  const SignUpAPI = (value: SignType) => AuthAPI.signup(value);
  const onSuccess = (response: unknown) => {
    if (isAuthResponseType(response)) storageToken(response);
  };

로직이 조금 달라지긴 했지만 더 명확하고 어떤 타입이 들어오는지, 네이밍까지 조금 더 확실해진거같아 잘 사용했다는 생각이 들었다.

 

unknown 타입

두 번째로 unknown이다. 기존에 타입스크립트를 사용하면서 겹치는 동시에 애매한 타입들에 대해선 전부 any로 넘겨버렸는데 unknown이라는 것을 알게되었다. 내용은 간단해서 인용해왔다.

 

typescript의 unknown과 any의 차이

Unknown과 any 두 타입 모두 타입을 지정하기 애매할때 사용한다. 두 타입의 차이점은 뭔지 궁금해서 찾아보았다. 아래 예시를 보자. function prettyPrint(x: unknown): string { if (Array.isArray(x)) { return "[" + x.ma

simsimjae.tistory.com

 

React-Query로 리팩토링

기존의 코드에서는 상태관리 툴을 딱히 사용하지않아 전부 props로 전달시켜줬는데 그렇게 하다보니 초반에 데이터를 처리하는 함수들을 전부 한 곳에 몰아넣었다.

 

아래 코드를 보면 Todo를 불러오고 추가,수정,삭제하는 함수들이 한 곳에 있었고 하나의 컴포넌트에서 받아 전부 뿌려주었다.

이렇게 하다보니 일단 보기가 안좋았고 API 요청과 다른 로직들이 더 추가되어야하는데 그냥 단순한 기능만 있어 이걸 기능에 따라 나눌 수 있을거같아 분리하긴했다

분리 전

import React from "react";
import API from "../lib/instance";
import { TodoDataType, TodoType } from "../types/todo";
import { todoSlice } from "../utils/todoSlice";

const useTodos = () => {
  const [todos, setTodos] = React.useState<TodoDataType[]>([]);

  const createTodo = (value: TodoDataType) =>
    setTodos((prev) => [...prev, value]);

  const updateTodo = (index: number) => (todo: TodoType) =>
    setTodos((todos) => {
      const { prev, next } = todoSlice(todos, index);
      const updatedTodo = Object.assign(todos[index], todo);
      return [...prev, updatedTodo, ...next];
    });

  const deleteTodo = (index: number) => () =>
    setTodos((todos) => {
      const { prev, next } = todoSlice(todos, index);
      return [...prev, ...next];
    });

  React.useEffect(() => {
    API.getTodos().then((response) => {
      setTodos(response.data.data);
    });
  }, []);

  return { todos, createTodo, updateTodo, deleteTodo };
};

export default useTodos;

 

분리 후

조금 더 명확해진거같은 느낌이 든다.

 

추가로 해야할 부분과 회고

구조, 설계를 고민하는게 가장 어려웠다. 기능 하나를 다 지우고 다시 만들까 하는 생각도 많이 했다. 그럼에도 그냥 되는 부분부터 천천히 수정하니 보이는게 많았고 생각대로 구현하긴 했다. 하지만 아직 많이 남았고. 어떻게 해야하나 하는 부분도 많다. 그럼에도 조금씩 진행해보면 어느정도 윤곽이 잡히지 않을까 생각하고 있다. 

 

두 번째로 배운걸 써먹는다는게 참 어려웠다. 기존에 내가 사용하는 코드들이 그냥 내가 익숙해서 사용하는 코드인 것처럼 느껴졌고, 익숙한 부분을 최대한 배운대로 수정하려하니 다른 것들이 눈에 띄고 섣불리 손대기 어려워졌다. 계속 이런 구조가 반복되서 머릿속에서 쓸데없이 커진거같다.. 조금 작게 나눠서 하나에 집중해보고 열정으로 머리 박는 것보단 잘 정리해서 계속 시도라도 해보는게 좋은 방향이 될꺼같다고 생각한 TIL이었다

복사했습니다!