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을 받아오기
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에서는 props
에 children
을 받아오는 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
타입을 정의한 interface
인 FunctionComponent
에서 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
라는 타입을 새로 생성해주고, 구조 분해 할당이 적용되는 변수 id
와 value
에 해당 타입을 지정해주면 원하는 값을 받아올 수 있다.
이처럼 구조 분해 할당을 이용해줘야하는 부분이 생긴다면, 변수에 배치되는 형태에 맞게 타입을 생성하여 정의해주도록 하자.
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를 관리하는 store
의 getState()
를 통해서 useSelector
를 가져온다는 것을 알 수 있고, useSeletor
를 통해 가져오는 state
의 타입은 어떠한 값이든 가능한 any
타입을 지정하고 있음을 알 수 있다.
📁 참고 자료
typescript react에서 styled-component 사용하기 (Theme Provider)
styled-components: API Reference