Combining React & TypeScript

김동현·2022년 8월 11일
0

TypeScript

목록 보기
18/18
post-thumbnail

프로젝트 생성

React 프로젝트에서 Typescript를 함께 사용하는 프로젝트를 생성하기 위해서 아래와 같은 명령어로 프로젝트 생성이 가능합니다.

npx create-react-app <projectname> -- template typescript

그러면 위 그림과 같은 프로젝트가 생성된 것을 확인할 수 있습니다. 타입스크립트를 사용하기 때문에 타입스크립트 컴파일러를 설정하는 tsconfig.json 파일도 존재하는 것을 확인할 수 있습니다.

src 폴더 내에는 여러 개의 ".tsx" 확장자를 가진 파일들이 존재하는 것을 확인할 수 있습니다. ".tsx" 파일은 타입스크립트 파일 내 JSX 문법을 사용한다고 명시하여 불필요한 경고창을 발생하지 않도록 해줍니다.

@types 패키지

프로젝트 내 package.json 파일의 dependencies에 typescript를 사용하기 위한 추가적인 패키지도 설치된 것을 확인할 수 있습니다.

"@type/*" 패키지들은 번역기 역할을 하는 패키지입니다. 즉, 자바스크립트 패키지를 타입스크립트 패키지로 변환해주는 역할을 하는 패키지입니다.

"react", "react-dom"과 같은 패키지들은 자바스크립트로 작성된 패키지로 이를 타입스크립트에서 사용하기 위해서는 "@types/react", "@types/react-dom"과 같은 패키지를 사용해야 합니다.

React.FC

App 컴포넌트 파일 내 App의 타입이 "React.FC"로 지정된 것을 볼 수 있습니다. 이는 "@types/react" 패키지가 제공하는 타입이며, FC 타입 이외 리액트에서 사용되는 여러 타입을 제공해줍니다

FC란 Function Component의 약자입니다. "React.FC" 대신 "React.FunctionComponent" 타입으로도 사용할 수도 있습니다.


"React.FC" 타입은 컴포넌트 함수 타입를 의미하며 컴포넌트 함수의 조건을 만족해야 합니다. 컴포넌트 함수는 반드시 하나의 리액트 엘리먼트를 반환해야 합니다.

만약 반환값이 리액트 엘리먼트가 아닌 경우 타입스크립트 컴파일러가 에러를 발생시킵니다.

props.children

참고로 React 18v부터는 React.FC 타입 내 children 관련 선언이 제거되었습니다.

18v 이전에는 React.FC가 props.children 프로퍼티를 옵셔널 프로퍼티로서 제공했었지만 옵셔널 프로퍼티로 인해 chlidren 프로퍼티가 꼭 필요하지만 값을 전달하지 않아도, 혹은 children 프로퍼티가 필요하지 않지만 참조가 가능했습니다.

React 18v부터는 children 프로퍼티를 사용해야하는 경우 명시적으로 children 프로퍼티 선언을 따로 해주어야 합니다.

const Component: React.FC<{ children: React.ReactNode }> = (props) => {
    ,,,
    props.children // -> 참조 가능. 리액트 엘리먼트 타입을 가짐
    ,,,,
}

React.FC 타입은 "제네릭 타입"이며, props 객체 타입을 전달할 수 있습니다.

위 코드에서는 props 객체는 children 프로퍼티를 갖고 있으며 프로퍼티 값의 타입은 React.ReactNode , 즉 리액트 엘리먼트라고 명시하고 있습니다.

React.ReactNode

"React.ReactNode" 타입일반적으로 props.children 프로퍼티 값의 타입으로 사용되는 타입입니다.

"React.ReactNode" 타입은 아래와 같이 정의되어 있습니다.

type ReactNode = ReactElement | string | number | ReactFragment | ReactPortal | boolean | null | undefined;

ReactNode 타입은 JSX 내에서 사용할 수 있는 모든 요소의 타입을 의미하며 원시 타입(number, string, boolean, null, undefined)까지 포함하는 타입입니다.

React.ReactElement

"React.ReactElement" 타입React.createElement 호출문으로 생성된 리액트 엘리먼트를 나타내는 타입입니다.

"React.ReactElement" 타입은 아래와 같이 정의되어 있습니다.

interface ReactElement<P = any, T extends string | JSXElementConstructor<any> = string | JSXElementConstructor<any>> {
    type: T;
    props: P;
    key: Key | null;
}

즉, "React.ReactNode" 타입과 달리 원시 타입인 string, boolean, null, undefined 타입을 포함하지 않습니다.

React.ReactChild

"React.ReactChild" 타입React.createElement 호출문으로 생성한 리액트 엘리먼트 타입인 "React.ReactElement" 타입과 string, number 타입만을 허용하는 타입입니다.

type ReactChild = ReactElement | string | number;

타입별로 허용되는 범위는 아래와 같습니다.

타입 범위 : "React.ReactNode" > "React.ReactChild" > "React.ReactElement"

Ref

useRef 훅을 사용하여 돔 노드 객체에 직접 접근하거나, 리렌더링에 영향을 받지 않고 독립적으로 관리되는 변수를 사용하기 위해서 사용되는 훅입니다.

useRef인수로 current 프로퍼티에 바인딩될 초기값을 전달할 수 있고, useRef 훅은 실제로 제네릭 함수로서 current 프로퍼티에 바인딩될 값의 타입을 제네릭으로 전달할 수 있습니다.

// useRef 훅은 제네릭 함수
// 제네릭으로 current 프로퍼티에 값의 타입 전달 가능
// 인수로 current 프로퍼티에 바인딩될 초기값 전달 가능

const ref = useRef<type>(initialValue);

DOM 노드 객체 접근

만약 돔 노드 객체에 접근하기 위해 사용할 때 useRef 훅 호출시 초기값으로 null을 전달하고, 제네릭 타입으로 연결될 돔 노드 객체의 타입을 전달하면 타입스크립트 컴파일러는 current 프로퍼티가 제네릭으로 전달한 타입과 undefined 타입을 유니온 타입을 갖고 있다고 판단하게 됩니다.

import { useRef } from 'react';

const Component: React.FC = () => {
    const inputRef = useRef<HTMLInputElement>(null);
    
    const printValueHandler = () => {
        // inputRef.current의 타입은 "HTMLInputElement | null" 타입
        console.log(inputRef.current?.value);
    };

    return <input ref={inputRef} onChange={printValueHandler} type="text" />;
}

위 코드에서 타입스크립트 컴파일러는 printValueHandler 이벤트 핸들러 내 inputRef.current에는 "HTMLInputElement" 타입의 객체가 아닌 "HTMLInputElement | undefined" 타입의 객체가 바인딩되어 있다고 인지합니다. 그러므로 value 프로퍼티에 접근하기 위해서는 옵셔널 체이닝(".?") 또는 ! 형 변환을 사용해야 합니다.

useRef 훅의 오버로딩

"@types/react"의 "index.d.ts"에 useRef 훅이 3개의 오버로딩을 가지는 것을 확인할 수 있습니다.

useRef 훅은 2가지 객체 타입을 반환하는데 "MutableRefObject""RefObject" 객체 타입을 반환합니다.

MutableRefObject & RefObject

// 1. MutableRefObject

interface MutableRefObject<T> {
    current: T;
}

"MutableRefObject" 타입은 current 프로퍼티 타입이 제네릭 타입 T이입니다.

current 프로퍼티 참조, 변경 모두 가능한 객체 타입입니다.


// 2. RefObject

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

"RefObject" 타입은 current 프로퍼티 타입이 제네릭 타입 T 또는 null 타입을 갖게 됩니다.

current 프로퍼티가 읽기 전용 프로퍼티로 읽기만 가능하며 변경은 불가능합니다.

1. useRef<T>(initialValue: T): MutableRefObject<T>

인수로 전달한 초기값(initialValue)의 타입과 제네릭 타입 T가 일치하는 경우 "MutableRefObject" 타입의 객체를 반환합니다.
반환되는 객체의 current 프로퍼티의 타입은 "T" 타입을 갖게 됩니다.

MutableRefObject 타입으로 반환되는 객체의 current 프로퍼티 값 읽기 쓰기 모두 가능합니다. 이는 current 프로퍼티 값 자체를 변경 가능하다는 의미입니다.

만약 current 프로퍼티 값 자체를 수정하고싶은 경우 useRef 훅 호출시 전달한 인수 타입과 제네릭 타입을 일치시켜 호출합니다.

2. useRef<T>(initialValue: T | null): RefObject<T>

인수로 전달한 초기값의 타입이 null을 허용하는 타입인 경우 "RefObject" 타입의 객체를 반환합니다.
이때 반환되는 객체의 current 프로퍼티의 타입은 "T | null" 타입을 갖는 프로퍼티가 됩니다.

RefObject 타입으로 current 프로퍼티는 읽기 전용 프로퍼티로서 current 프로퍼티 값 읽기는 가능하지만 변경은 불가능합니다.

참고로 변경 불가능한 것은 current 프로퍼티 값 자체이며, 만약 current 프로퍼티에 바인딩된 값이 객체인경우 객체의 프로퍼티 값은 변경이 가능합니다.

만약 current 프로퍼티 값 자체는 수정이 불가능하도록 만들고 싶은 경우 인수로 null을 전달하여 호출합니다.

3. useRef<T = undefined>(): MutableObject<T | undefined>

제네릭 타입으로 undefined또는 전달하지 않은 경우 "MutableObject" 객체를 반환하며, 반환되는 객체의 current 프로퍼티의 타입은 "T | undefined" 타입을 갖게 됩니다.

정리

  1. 돔 노드 객체에 접근
    : useRef인수로 null을 전달하여 호출하고(2번 오버로딩), 제네릭 타입으로는 연결될 돔 노드 객체의 타입을 전달해줍니다. current 프로퍼티 값은 변경 불가능한 읽기 전용 프로퍼티가 됩니다.
    주의할 점으로 current 프로퍼티 타입이 "T | null" 타입으로 null 타입을 포함하는 것에 주의해야 합니다.

  2. 로컬 변수로 사용
    : current 프로퍼티를 통해 리렌더링에 영향을 받지 않는 독립적인 변수를 사용할 때는 useRef인수로 사용할 값을 전달하고, 제네릭 타입 으로는 사용할 값의 타입을 작성해줍니다.
    반환된 객체는 current 프로퍼티 값 참조, 변경 모두 가능한 객체를 반환하게 됩니다.

State

useState 훅으로 컴포넌트의 상태를 추가할 수 있습니다. 이때 상태값에 대한 구체적인 타입을 지정하여 타입스크립트에게 알려주어야 합니다.

구체적인 상태 타입을 알려주기 위해서 useState 훅도 제네릭 함수로 사용할 수 있으며 제네릭 타입으로 구체적인 상태값 타입을 작성하여 타입스크립트에게 알려줍니다.

// useState 훅은 제네릭 함수
// 제네릭 타입으로 구체적인 상태값 타입을 전달
const [ state, setState ] = useState<type>(initialState)

위 코드에서는 useState 훅을 사용하여 todos 라는 상태 변수를 사용하려고 합니다. 이때 상태값의 초기값으로 빈 배열을 전달하고, todoHandler 함수 내부에서 setTodos 상태 변경 함수를 호출하여 상태를 변경하려고 하는데 에러가 표시됩니다.

이는 타입스크립트가 todos 라는 상태값이 언제나 빈배열이라 인지하고 추가적인 요소를 가지지 않는다고 기대하고 있기 때문에 위와같은 에러가 발생하게 됩니다.

그러므로 우리는 타입스크립트에게 상태값에 대한 구체적인 타입을 설정하기 위해서 useState 훅을 제네릭 함수로 사용하여 제네릭 타입으로 상태값의 타입을 전달해주어야 합니다.

위 코드에서 useState<{id: string; text: string;}[]>([]);처럼 사용함으로써 상태값의 초기값은 빈 배열이지만 상태값은 구체적으로 {id: string; text: string;} 객체 타입을 요소로 갖는 배열임을 제네릭 타입으로 전달해줍니다.

Event Type

타입스크립트는 이벤트 핸들러가 이벤트 핸들러인지 판단할 수 없습니다. 그러므로 이벤트 핸들러 인수로 전달되는 이벤트 객체 또한 인지하지 못하여 어떤 인수가 전달되는지 모릅니다.

그러므로 우리는 이벤트 핸들러가 인수로 전달받는 이벤트 타입에 대해서 명시적으로 작성하여 타입스크립트에게 알려주어야 합니다.

이벤트 객체 또한 "@types/react" 패키지가 제공하고 있습니다.
form 요소에 submit 이벤트 발생시 이벤트 객체는 React.FormEvent<HTMLElement>, input 요소에 change 이벤트 발생시 발생한 이벤트 객체는 React.ChagneEvent<HTMLElement> 을 사용합니다.

해당 이벤트 객체들은 제네릭 타입으로 사용하며 제네릭으로 이벤트를 발생한 돔 요소 노드 객체 타입을 전달할 수 있습니다.
제네릭을 사용함으로써 event.target에 바인딩될 돔 요소 노드 객체 타입을 타입스크립트에게 알려줄 수 있습니다. 만약 event.target에 접근할 필요가 없는 경우 전달하지 않아도 됩니다.

// form 요소에 발생한 submit 이벤트 -> React.FormEvent<type>
const submitHandler = (event: React.FormEvent) => {
    ,,,,
};


// input 요소에 발생한 change 이벤트 -> React.ChangeEvent<type>
const changeInputHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
    ,,,
}

이 외에도 React.MouseEvent<HTMLElement>, React.KeyboardEvent<HTMLElement>, React.FocusEvent<HTMLElement>, React.DragEvent<HTMLElement> 등 여러 이벤트 객체들이 존재합니다.

Context

React의 Context API가 제공하는 createContext 메서드도 제네릭 함수로 동작합니다. 제네릭 타입 전달할 때 context가 가질 값을 전달해줍니다.

import { createContext } from 'react';

const Context = createContext<type>(defaultValue);

const ContextProvider: React.FC = (props) => {
    ,,,
    return (
        <Context.Provider value={...}>
            {props.children}
        </Context.Provider>
    );
};

createContext 호출할 때 기본값을 인수로 전달하고, 제네릭으로 구체적인 타입을 작성해주어야 합니다.

profile
Frontend Dev

0개의 댓글