방구석에 놔둔 개발 노트

1년차 웹 / 앱 프론트엔드 엔지니어의 좌충우돌 얼렁뚱땅 앞뒤짱구 생존기

2023-01-15: TypeScript 학습 (5)

2023-01-15: TypeScript 학습 (5)


TypeScript 실습편, 작은 CRUD 프로젝트 만들어보기.

이전 시간에 TodoList를 만들어본 경험을 토대로 이번엔 스스로 작은 CRUD 프로젝트를 만들어보고자 한다.

이번 프로젝트의 목적은 기존의 학습을 복습하고, 추가적으로 라이브러리들을 설치하면서 이를 어떻게 TypeScript와 묶을 것인지, 그리고 자주 쓰는 JavaScript의 문법을 TypeScript에서는 어떻게 정의하고 사용할지를 정리해보고자 한다.

목차

1️⃣ TypeScript와 Redux가 포함된 Create-React-App 패키지 설치

2️⃣ 추가 환경 설치 및 적용 (styled-components의 예)

3️⃣ type을 생성하여 children prop을 받아오기

4️⃣ 구조 분해 할당 문법에서의 타입 정의

5️⃣ (styled-components) 공통 컴포넌트의 부분 스타일 적용

6️⃣ (Redux Toolkit) useSeletor의 매개변수 타입 지정

1️⃣ TypeScript와 Redux가 포함된 Create-React-App 패키지 설치

https://github.com/reduxjs/cra-template-redux-typescript

위의 레포지토리의 README.md를 통해 아래의 터미널 명령어를 확인할 수 있다.

npx create-react-app my-app --template redux-typescript

# or

yarn create react-app my-app --template redux-typescript

내용대로 터미널에 설치해서 프로젝트를 만들어주자.

2️⃣ 추가 환경 설치 및 적용 (styled-components의 예)

이전에는 기존 템플릿 환경에서만 사용했다면, 이번에는 추가적인 라이브러리나 프레임워크를 직접 설치해봤다.

React로 CSS를 관리할 때는 CSS in JS 라이브러리인 styled-components를 많이 활용했기 때문에, 여기서도 해당 패키지를 설치해보고자 한다.

우선 아래와 같이 터미널 명령어를 통해 라이브러리를 설치해주자.

npm install styled-components

설치가 완료했다면, 이제 .tsx 파일을 하나 만들어 styled 컴포넌트를 하나 생성해보자. 해보면 .jsx 때와는 다르게 정상적으로 styled-components의 인식이 잘 안 될 것이다.

package.json 파일을 열어 의존성(dependencies) 내역들을 확인해보면,

위와 같이 리스트가 나타나는데, TypeScript의 경우에는 JavaScript 라이브러리가 TypeScript로 번역해주는, 즉 컴파일해주는 @types 라이브러리가 필요한데 현재 styled-components에는 의존성에 과련 패키지가 존재하지 않는다.

따라서 아래의 터미널 명령어를 입력해 설치해주자.

npm install @types/styled-components

여기까지 했으면, 일단락되겠지만 경우에 따라서는 위의 리스트에 추가되지 않는 경우가 있다. 그 때는 설치한 해당 패키지의 버전을 확인 후, 의존성에 추가해주도록 하자.

현재 설치된 라이브러리나 프레임워크 패키지의 버전을 알 수 있는 명령어는 아래와 같다.

npm show (라이브러리 혹은 프레임워크 패키지명) version

여기서는 @types/styled-components의 버전을 확인했으며 확인 후 아래와 같이 의존성에 추가했다.

이제 다시 아까 생성했던 .tsx 파일에 styled 컴포넌트를 생성해보면 styled가 정상적으로 적용되는 것을 볼 수 있다.

3️⃣ type을 생성하여 children prop을 받아오기

React 18 버전부터 React 내의 TypeScript 타입 정의에 Function Components에서는 propschildren을 받아오는 PropsWithChildren 항목이 빠져있다.

따라서, children을 받아오게 하기 위해서는 children을 받아올 수 있는 타입을 새로 만들어내고, 이를 Function Components에 적용했다.

export type Props = {
  children?: React.ReactNode;
};

React에 있는 노드들을 가리키는 React.ReactNode라는 타입을 정의해줌으로서 Props라는 타입을 새로 생성해줬다.

이제 이렇게 지정한 타입을 export를 붙여줌으로서 여기저기에 사용할 수 있게 했으므로, 아래의 Layout 컴포넌트에 해당 타입을 제네릭 타입으로 지정했다.

import React from "react";
import styled from "styled-components";
import { Props } from "../App";

const Layout: React.FC<Props> = (props) => {
  return <LayoutContainer>{props.children}</LayoutContainer>;
};

export default Layout;

Props를 제네릭 타입으로 정의해놨기 때문에 이제 매개변수인 props를 통해 chilren에 접근할 수 있게 됐으므로, 위의 코드와 같이 children을 불러오는 것이 가능해졌다.

위의 방식이 싫다면, 아래와 같이 index.d.ts 파일에서 React.FC 타입을 정의한 interfaceFunctionComponent에서 props의 타입 정의에 PropsWithChildren을 붙여주자.

type FC<P = {}> = FunctionComponent<P>;

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

4️⃣ 구조 분해 할당 문법에서의 타입 정의

JavaScript에서는 구조 분해 할당 문법을 사용할 때, 아래와 같이 적용했다.

const nameObj = { name1: "viliage", name2: "city" }

const { name1, name2 } = nameObj
console.log(name1); // 'viliage'
console.log(name2); // 'city';

그런데, TypeScript에서 위의 구조 분해 할당 문법을 사용할 때, 타입을 어떻게 정의해줘야할까?

타입 별칭을 이용해 타입을 생성해주고, 해당 타입을 구조 분해 할당을 사용할 부분에 정의를 내려주면 된다.

type EventObject = {
  id: string;
  value?: string;
};

const inputUserData = (e: React.ChangeEvent) => {
          const { id, value }: **EventObject** = e.target;
  setUserData({ ...userData, [id]: value });
};

위와 같이 EventObject라는 타입을 새로 생성해주고, 구조 분해 할당이 적용되는 변수 idvalue에 해당 타입을 지정해주면 원하는 값을 받아올 수 있다.

이처럼 구조 분해 할당을 이용해줘야하는 부분이 생긴다면, 변수에 배치되는 형태에 맞게 타입을 생성하여 정의해주도록 하자.

5️⃣ (styled-components) 공통 컴포넌트의 부분 스타일 적용

styled-components 라이브러리를 사용하다보면, 공통적으로 사용하는 UI 컴포넌트를 만들 때 특정 부분만 스타일을 변경해줘야 하는 경우가 있을 것이다.

JavaScript로 구현할 때는 그냥 props를 붙여서 사용하면 되겠지만은, TypeScript을 쓰고 있으면 그 props에 타입을 지정해줘야하는 경우가 생긴다.

그 경우에는 부분적으로 변경할 style만을 정의할 타입을 생성해주고, 이를 styled-components의 컴포넌트에 제네릭 타입으로서 지정해주면 된다.

type StyledProps = {
  width: string;
    // 컴포넌트의 props에 써줘야하는 style이다.
  margin?: string;
    // ?가 들어가있으므로 선택적으로 써줘야하는 style이다.
};

    const CommentFormInputArea = styled.div<StyledProps>`
    display: flex;
    flex-direction: column;
    width: ${(props) => props.width || "100%"};
    margin: ${(props) => props.margin || "0"};
    box-sizing: border-box;
  `;

.
.
.

<CommentFormInputArea width="90%" margin="0.5rem 0"> </CommentFormInputArea>
// 위와 같이 사용할 수 있다.

6️⃣ (Redux Toolkit) useSeletor의 매개변수 타입 지정

Redux Toolkit 라이브러리를 사용하다보면 state에 있는 값을 불러오기 위해 useSelector를 사용해야한다.

이때, useSelector에 들어갈 매개변수는 콜백 함수이며, 이 콜백 함수 내에서도 매개변수와 반환값이 존재하는데 흔히 state라고 많이 쓰는 이 콜백 함수의 매개변수에는 타입을 지정해줘야 한다.

이 때 사용하는 타입이 RootState이다.

const commentState = useSelector((state:RootState) => state.comment.commentList)

그렇다면 이 RootState는 어디서 기원된 것일까?

코드를 파다보면 아래와 같이 타입이 지정되어 있음을 볼 수 있다.

export type RootState = ReturnType<typeof store.getState>;
// store에 있는 getState가 받아올 타입 값을 RootState의 타입으로 지정했다.

// 좀 더 거슬러 올라가 index.d.ts 파일을 들춰보자.
export interface Store<S = any, A extends Action = AnyAction> {
  dispatch: Dispatch<A>
  getState(): S
    // 실직적으로 어떤 타입이든 받아올 수 있도록(any) 해놨다.

  subscribe(listener: () => void): Unsubscribe

  replaceReducer(nextReducer: Reducer<S, A>): void

  [Symbol.observable](): Observable<S>
}

이처럼, Redux를 관리하는 storegetState()를 통해서 useSelector를 가져온다는 것을 알 수 있고, useSeletor를 통해 가져오는 state의 타입은 어떠한 값이든 가능한 any 타입을 지정하고 있음을 알 수 있다.

📁 참고 자료

typescript react에서 styled-component 사용하기 (Theme Provider)

styled-components: API Reference

@types/styled-components

구조 분해 할당 - JavaScript | MDN

TypeScript 환경에서 Redux를 프로처럼 사용하기