main

타입스크립트 제대로 톺아보기 IV

Log
12

저번에는 axios 라이브러리 타입을 까보았다면 오늘은 리액트 라이브러리 타입에 대해서 알아볼까합니다.

📌 FC vs VFC, props 타이핑

17버전까지는 VFC가 있었는데, 18버전 이후로는 deprecated되었습니다. 왜 그런지에 대해서 알아볼까요?

interface VoidFunctionComponent<P = {}> {
  (props: P, context?: any): ReactElement<any, any> | null;
  propTypes?: WeakValidationMap<P>;
  contextTypes?: ValidationMap<any>;
  defaultProps?: Partial<P>;
  displayName?: string;
}

원래 VFCVoidFunctionComponent의 약자로, VFS는 children을 가지지않는 함수로 쓰였으며, 이전에 FC는 children을 가지는 함수로 쓰였습니다. 하지만 18버전 이후로는 children이 옵셔널 타입이었기 때문에, 오해의 소지?가 있었어서 모두 VFC나 SFC같은 컴포넌트 타입이 모두 사라지게 되었고 children 속성이 정의되지 않은 FC 컴포넌트 타입만이 남게 되었습니다. 따라서 FC는 기본적으로 children이 없기 때문에, children 속성을 설정해주어야합니다.

현재는 기명함수에는 FC 타입을 줄 수 없습니다.

// 1. 익명 함수를 변수에 할당하여 타이핑하기
const UserBox: FC<UserProps> = function ({ name, cart }) {
  return ( ... );
};
 
// 2. 화살표 함수 사용하기
const UserBox = FC<UserProps> = ({name, cart}) => {
  return (...) ;
}

기명 함수에서 prop관련된 제네릭이나 타입을 지정해주고 싶다면 아래와 같이 설정해주어야합니다.

interface Props {
  name: string;
}
 
function Component({ name }: Props){
  return (
    <div>
      <div>안녕하세요</div>
      <span>{ name }</span>
    </div>
  )
}

만약 children을 받는 컴포넌트라면 직접적으로 children을 명시해주어야합니다.

interface Props {
  name: string;
  children: React.Node;
}
 
function Component({ name, children }: Props){
  return (
    <div>
      <div>안녕하세요</div>
      { children }
    </div>
  )
}

만약 children이 옵셔널이라면, children을 받지 않아야하는 컴포넌트라도 children을 쓰게 되면 에러를 발생하지 않기 때문에 18버전 이후로 부터는 직접적으로 명시해주어야합니다.


📌 useState, useEffect

useState는 값을 초기화할 때, 초기 제네릭을 설정해주면 됩니다.

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];

useState의 타입을 보면 초기값은 함수도 올 수 있습니다. 복잡한 함수인 경우 처음에만 적용하고 싶을 때 (리렌더링 제외) 함수를 넣어주면 됩니다. 이를 lazy init 이라고 합니다.

import { useState } from 'react';
 
function Component() {
  const [state1, setState1] = useState<number>(1234);
  const [state2, setState2] = useState<number>(() => 1 + 2);
 
  return (
    <div>hi</div>
  )
}

useEffect는 effect 콜백함수의 리턴 값이 void로 픽싱되어있기 때문에 async를 사용할 수 없습니다.
async를 사용하는 순간 promise를 반환하기 때문입니다.
실제 js에서는 사용할 수 있습니다.

type EffectCallback = () => void | Destructor;
useEffect(async ()=> {
  const response = await axios.get('~~');
  setState(response.data);
}, [])

위 경우는 타입스크립트에서 사용할 수 없습니다. 왜냐면 현재 promise를 반환하고 있기 때문입니다.

useEffect(()=> {
  const getData = async () => {
    const response = await axios.get('~');
    return response;
  }
  setState(getData.data);
}, [])

따라서 위와 같은 식으로 내부 함수를 만들어서 반환하는 식으로 강제됩니다.


📌 브랜딩 기법

만약에 유료를 달러로 환전해서 돈계산을 하고 싶을 때, 아래와 같은 함수를 만들 수 있는데요.

const usd = 10
const eur = 10
const krw = 2000
 
function euroToUsd(euro: number): number {
  return euro * 1.18;
}
 
euroToUsd(krw);

하지만 유로 -> 달러 전환만 가능한 함수인데, 한화 krw를 넣어도 문제가 되지 않습니다. 왜냐면 매개변수의 타입으로 number을 받고 있기 때문입니다. 하지만 이런 경우, euro인 값만 받고 싶을 때 어떻게 할까요?

같은 타입 내에서도 어떤 표시를 추가하고 싶을 때, 뭔가 값에 네임택을 달고 싶을 때 필요한게 브랜딩 기법입니다.

type Brand<K, T> = K & { __brand: T }; 
type Brand<K, T> = K & { __brand: T};
 
type EUR = Brand<number, 'EUR'>;
type USD = Brand<number, 'USD'>;
type KRW = Brand<number, 'KRW'>;
 
const usd = 10 as USD;
const eur = 10 as EUR;
const krw = 2000 as KRW;
 
function euroToUsd(euro: EUR): number {
  return euro * 1.18;
}
 
euroToUsd(krw); // 에러 발생

위에서 Brand라는 가짜 타입을 만들어서, 각각 타입 네임택을 달아주면 마지막 euroToUsd(krw)를 넣으면 에러를 발생하게 됩니다. 왜냐하면 매개변수에서 EUR 타입만 받고 있기 때문입니다.
각각에 타입을 달아주기 위해서는 타입단언(as)을 통해 타입을 달아주어야합니다.


📌 useCallback, useRef

useCallback

useCallback 같은 경우에는 17버전 이전에서는 매개변수와 리턴값에 대한 타이핑이 되어있었는데, 18부터는 사라지게 되었습니다.

// 17버전
function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
 
// 18버전
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
const onSubmitForm = useCallback((e: FormEvent<HTMLFormElement>) => {
    ....
  }, [word, value]);

위와 같이, 매개변수를 타입을 매번 넣어주어야합니다.

useRef

useRef는 React Hook의 일종으로, 인자로 넘어온 초깃값을 useRef 객체의 .current 프로퍼티에 저장합니다. DOM 객체를 직접 가리켜서 내부 값을 변경하거나 focus() 메소드를 사용하거나 하는 때에 주로 사용하고, 변경되어도 컴포넌트가 리렌더링되지 않도록 하기 위한 값들을 저장하기 위해서도 사용합니다. (이는 useRef가 내용이 변경되어도 이를 알려주지 않기 때문이다. .current 프로퍼티를 변경시키는 것은 리렌더링을 발생시키지 않고, 따라서 로컬 변수 용도로 사용할 수 있다.)

본질적으로 useRef는 .current 프로퍼티에 변경가능한 값을 담고 있는 것입니다.

interface MutableRefObject<T> {
    current: T;
}
 
interface RefObject<T> {
    readonly current: T | null;
}

타입에서도 알 수 있듯이 MutableRefObject타입은 변경될 수 있는 값을 담는 용도이며 RefObject같은 경우에는 DOM 객체를 직접 가르키는 용도로 사용됩니다. 왜냐면 DOM은 null인 경우도 있기 때문에, 타입에서 null처리를 해주는 것을 알 수 있습니다.

useRef는 3가지의 타입이 존재합니다.

  • useRef<T>(initialValue: T): MutableRefObject<T>
    인자의 타입과 제네릭의 타입이 T로 일치하는 경우, MutableRefObject<T>를 반환합니다.
    MutableRefObject<T>의 경우, 이름에서도 볼 수 있고 위의 정의에서도 확인할 수 있듯 current 프로퍼티 그 자체를 직접 변경할 수 있습니다.

  • useRef<T>(initialValue: T|null): RefObject<T>
    인자의 타입이 null을 허용하는 경우, RefObject<T>를 반환합니다.
    RefObject<T>는 위에서 보았듯 current 프로퍼티를 직접 수정할 수 없습니다. (readonly이기 때문에)

  • useRef<T = undefined>(): MutableRefObject<T | undefined>
    제네릭의 타입이 undefined인 경우(타입을 제공하지 않은 경우), MutableRefObject<T | undefined>를 반환합니다.

위의 예시를 코드로 보면 이해하기가 더 쉽습니다.

import React, { useRef } from "react";
 
const App = () => {
  const localVarRef = useRef<number>(0);
 
  const handleButtonClick = () => {
		if (localVarRef.current) {
	    localVarRef.current += 1;
	    console.log(localVarRef.current);
		}
  };
 
  return (
    <div className="App">
      <button onClick={handleButtonClick}>+1</button>
    </div>
  );
};
 
export default App;

위의 예시에서는 useRef의 제네릭으로 number0으로 초기값을 설정해주었는데요, 이런 경우 useRef<T>(initialValue: T): MutableRefObject<T> 에 해당합니다. 즉 첫번째에 해당되는 것입니다.
따라서 MutableRefObject는 변경이 가능한 값 (DOM이 아닌) 이기 때문에 current에 직접적으로 접근하여 값을 수정할 수 있습니다.

  const localVarRef = useRef<number>(null);
 
  const handleButtonClick = () => {
    localVarRef.current += 1;
    console.log(localVarRef.current);
  };
...

하지만 초기값을 null로 설정하게 되면 useRef<T>(initialValue: T|null): RefObject<T>의 경우로 해당되어버립니다. 따라서 current를 직접적으로 설정할 수 없습니다. (readonly이기 때문에)

따라서 DOM 엘리먼트를 ref로 가져오고 싶은 경우에는 아래와 같이 설정해주어야합니다.

const App = () => {
  const inputRef = useRef<HTMLInputElement>(null);
 
  ...
}

제네릭에는 ref를 달아줄 HTMLElement 종류를 명확히 넘겨주고, null로 초기값을 설정해주어야합니다. 이때 null로 설정해주지 않는다면 이때는 useRef<T = undefined>(): MutableRefObject<T | undefined> 세번째 경우에 해당되어버리기 때문에 (undefined) MutableRefObject 타입이 되어버리기 때문에 current를 수정할 수 있게 됩니다.

🧐 useRef에 대한 정리

정리하면 다음과 같습니다.

const localVarRef = useRef<number>(0);

로컬 변수 용도로 useRef를 사용하는 경우, MutableRefObject<T>를 사용해야 하므로 제네릭 타입과 같은 타입의 초깃값을 넣어주면 됩니다.

const inputRef = useRef<HTMLInputElement>(null);

DOM을 직접 조작하기 위해 프로퍼티로 useRef 객체를 사용할 경우, RefObject<T>를 사용해야 하므로 초깃값으로 null을 넣어주면됩니다.


김다은 이모지
Daeun Kim
Junior Frontend Engineer