[TS] #5 React에서 TS 사용하기
1. 함수형 컴포넌트 타입 지정: React.FC
React.FC
(React.FunctionComponent)는 TypeScript와 React를 사용할 때 함수형 컴포넌트를 정의하는 데 사용되는 타입이다.
import React from "react";
const App: React.FC = () => {
return <div>App</div>;
};
export default App;
2. Props 타입 지정
2.1 Props를 위한 타입 정의하기
아래와 같은 객체를 props로 다른 컴포넌트에게 전달하려면 어떻게 해야할까?
// 아래 코드는 오류 발생
import React from "react";
import PostsPage from "./PostsPage";
const postInfo = [
{
postId: 1,
title: "너무 더워요",
body: "오늘 날씨 30도☀️",
date: "2024-09-18",
tags: ["날씨", "여름"],
user: {
userId: 2,
username: "시은",
},
},
];
const App: React.FC = () => {
return (
<div>
<PostsPage postInfo={postInfo} />
</div>
);
};
export default App;
객체를 props로 다른 컴포넌트에 전달할 때, 먼저 해당 객체에 대한 타입을 정의해야 한다.
- 타입을 만들기 위해
src/model
이라는 폴더 안에posts.ts
라는 파일을 생성하자. - 타입을 생성하는 방법에는
type
,interface
으로 2가지 방법이 있다. type
은 잠재적인 오류의 위험이 있으므로 되도록이면interface
를 사용하고, 부득이한 상황(타입 앨리어싱)에만type
을 써주도록 하자
// src/model/posts.ts
export interface Post {
postId: number;
title: string;
body: string;
date: string;
tage: string[];
user: {
userId: number;
username: string;
};
}
아래와 같이
type
내에 다른 타입을 포함시킬 수도 있다. (추천)
// src/model/posts.ts
export interface Post {
postId: number;
title: string;
body: string;
date: string;
tags: string[];
user: User;
}
export interface User {
userId: number;
username: string;
}
2.2 컴포넌트에 타입 적용하기
위와 같이 정의한 타입을 컴포넌트에 적용하면 된다.
// src/App.tsx
import React from "react";
import PostsPage from "./PostsPage";
import { Post } from "./model/posts";
const postInfo: Post[] = [
{
postId: 1,
title: "너무 더워요",
body: "오늘 날씨 30도☀️",
date: "2024-09-18",
tags: ["날씨", "여름"],
user: {
userId: 2,
username: "시은",
},
},
];
const App: React.FC = () => {
return (
<div>
<PostsPage postInfo={postInfo} />
</div>
);
};
export default App;
2.3 Props 타입 지정하기
컴포넌트에 전달할 props의 타입을 정의하자.
// src/PostsPage.tsx
import React from "react";
import { Post } from "./model/posts";
// props의 경우 배열을 받을 수 있도록 타입 수정
interface PostsProps {
postInfo: Post[];
}
const PostsPage: React.FC<PostsProps> = ({ postInfo }) => {
return (
<ul>
{postInfo.map((post) => (
<li key={post.postId}>
<h2>{post.title}</h2>
<p>{post.body}</p>
<p>{post.date}</p>
<p>작성자: {post.user.username}</p>
</li>
))}
</ul>
);
};
export default PostsPage;
interface
이 아닌,type
로 type을 만드는 경우 아래와 같이 하면 된다.
type PostsProps = {
postInfo: Post[];
};
3. Generic 간단히 이해하기
제네릭(generic)이란 데이터의 타입을 일반화한다(generalize)하는 방법을 의미한다.
간단히 데이터의 타입을 변수화 하는 것이다! 아래와 같이 사용한다.
3.1 제네릭 타입 선언
type Generic<T> = {
someValue: T;
};
type Test = Generic<string>;
3.2 제네릭 함수 호출
function someFunc<T>(value: T) {}
someFunc<string>("hello");
즉, 타입을 생성할 때 원하는 타입을 인자로 받아 넣어주면 된다.
4. useState에서 TS 사용하기
useState에 제네릭 문법을 사용하여 타입을 지정한다.
useState<type>
와 같이 사용하면 된다.<>
는 제네릭 문법으로 useState를 부르는 순간 type을 지정해준다.- 원시 값(number, string)은 타입 추론이 되기 때문에 타입 지정을 안 해줘도 되지만, 참조 값(객체, 배열)은 어떠한 아이템이 담겼는지 알 수 없기 때문에 추론이 불가능하여 타입을 직접 지정해줘야 한다.
// App.tsx
import React, { useState } from "react";
import PostsPage from "./PostsPage";
import { Post } from "./model/posts";
const postInfo: Post[] = [
{
postId: 1,
title: "너무 더워요",
body: "오늘 날씨 30도☀️",
date: "2024-09-18",
tags: ["날씨", "여름"],
user: {
userId: 2,
username: "시은",
},
},
];
const App: React.FC = () => {
// useState에 제네릭 문법을 사용하여 타입을 지정
const [myPosts, setMyPosts] = useState<Post[]>(postInfo);
return (
<div>
<PostsPage postInfo={myPosts} />
</div>
);
};
export default App;
5. 함수 타입 지정하기
컴포넌트에 전달할 함수의 타입을 정의한다.
// src/App.tsx
import React, { useState } from "react";
import PostsPage from "./PostsPage";
import { Post } from "./model/posts";
const postInfo: Post[] = [
{
postId: 1,
title: "너무 더워요",
body: "오늘 날씨 30도☀️",
date: "2024-09-18",
tags: ["날씨", "여름"],
user: {
userId: 2,
username: "시은",
},
},
];
const App: React.FC = () => {
const [myPosts, setMyPosts] = useState<Post[]>(postInfo);
// 포스트 배열에서 특정 포스트의 title을 변경하는 함수
const changeTitle = (id: number, newTitle: string) => {
const updatedPosts = myPosts.map((post) =>
post.postId === id ? { ...post, title: newTitle } : post
);
setMyPosts(updatedPosts); // 변경된 배열을 setMyPosts로 업데이트
};
return (
<div>
<PostsPage postInfo={myPosts} changeTitle={changeTitle} />
</div>
);
};
export default App;
void는 TypeScript에서 반환 값이 없는 함수를 나타내는 타입이다.
// src/PostsPage.tsx
import React, { useState } from "react";
import { Post } from "./model/posts";
interface PostsProps {
postInfo: Post[];
changeTitle(id: number, newTitle: string): void;
}
const PostsPage: React.FC<PostsProps> = ({ postInfo, changeTitle }) => {
const [newTitle, setNewTitle] = useState<string>("");
return (
<ul>
{postInfo.map((post) => (
<li key={post.postId}>
<h2>{post.title}</h2>
<p>{post.body}</p>
<p>{post.date}</p>
<p>작성자: {post.user.username}</p>
</li>
))}
</ul>
);
};
export default PostsPage;
6. Event Handler 사용하기
이벤트 핸들러에서 TS를 사용할 때는 event에 대한 type을 지정해야한다.
따라서, 이벤트 핸들러를 인라인으로 작성하고 e에 마우스를 올려보면 어떤 type을 썼는지 타입 추론이 가능하다.
return <div onChange={(e) => {}}>{}</div>;
type 복사 붙여넣기!
const TodoInput = () => {
const handleOnSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
return (
<section>
<form onSubmit={handleOnSubmit}>
<input type="text" name="title" />
<input type="text" name="content" />
<button type="submit">제출</button>
</form>
</section>
);
};
export default TodoInput;
7. interface extends 사용하기
interface extends
는 TypeScript에서 인터페이스를 확장할 때 사용하는 구문이다.
이를 통해 기존 인터페이스의 속성이나 메서드를 유지하면서도 추가적인 속성을 정의할 수 있다.
아래
User
타입에 새로운 타입을extends
로 확장해보자.
// src/model/posts.ts
export interface Post {
postId: number;
title: string;
body: string;
date: string;
tags: string[];
user: User;
}
export interface User {
userId: number;
username: string;
}
이제
UserPage
컴포넌트에 확장된UserProps
타입을 적용하자.
User
인터페이스를 확장한 UserProps
에 추가 속성 email, lastLogin, isActive, 그리고 메서드 showUserName을 정의하고 UserPage에서 props로 받아 사용 한다.
// src/UserPage.tsx
import React from "react";
import { User } from "./model/posts";
interface UserProps extends User {
email: string;
lastLogin?: string;
isActive: boolean;
showUserName(name: string): string;
}
const UserPage: React.FC<UserProps> = ({
userId,
username,
email,
lastLogin,
isActive,
showUserName,
}) => {
return (
<div>
<h2>User Information</h2>
<p>User ID: {userId}</p>
<p>Username: {username}</p>
<p>Email: {email}</p>
<p>Last Login: {lastLogin || "로그인 정보 없음"}</p>
<p>Account Status: {isActive ? "Active" : "Inactive"}</p>
</div>
);
};
export default UserPage;
마지막으로 App 컴포넌트에서 확장된
UserProps
을 적용하자.
App
컴포넌트에서 UserPage
에 확장된 속성들을 props로 전달하면 된다.
// src/App.tsx
import React, { useState } from "react";
import PostsPage from "./PostsPage";
import { Post } from "./model/posts";
import UserPage from "./UserPage";
const postInfo: Post[] = [
{
postId: 1,
title: "너무 더워요",
body: "오늘 날씨 30도☀️",
date: "2024-09-18",
tags: ["날씨", "여름"],
user: {
userId: 2,
username: "시은",
},
},
];
const App: React.FC = () => {
const [myPosts, setMyPosts] = useState<Post[]>(postInfo);
const changeTitle = (id: number, newTitle: string) => {
const updatedPosts = myPosts.map((post) =>
post.postId === id ? { ...post, title: newTitle } : post
);
setMyPosts(updatedPosts);
};
const showUserName = (userName: string) => {
return userName;
};
return (
<div>
<PostsPage postInfo={myPosts} changeTitle={changeTitle} />
<UserPage
userId={3}
username="시은"
email="sieun@example.com"
isActive={true}
showUserName={showUserName}
/>
</div>
);
};
export default App;
7. Omit과 Pick
7.1 Omit
Omit<T, K>
는 타입T
에서 속성K
를 제외한 새로운 타입을 생성하는 TypeScript 유틸리티 타입이다.
// src/UserPage.tsx
import React from "react";
import { User } from "./model/posts";
// User 인터페이스에서 `userId`를 제외하고 새로운 속성을 추가한 `UserProps` 타입을 정의
interface UserProps extends Omit<User, "userId"> {
email: string;
lastLogin?: string;
isActive: boolean;
showUserName(name: string): string;
}
const UserPage: React.FC<UserProps> = ({
// userId는 Omit으로 제외됨
username,
email,
lastLogin,
isActive,
showUserName,
}) => {
return (
<div>
<h2>User Information</h2>
{/* userId는 Omit으로 제외되어 사용하지 않음 */}
<p>Username: {username}</p>
<p>Email: {email}</p>
<p>Last Login: {lastLogin || "로그인 정보 없음"}</p>
<p>Account Status: {isActive ? "Active" : "Inactive"}</p>
</div>
);
};
export default UserPage;
7.2 Pick
Pick<T, K>
는 타입T
에서 속성K
만 선택하여 새로운 타입을 생성하는 TypeScript 유틸리티 타입이다.
// src/PostsPage.tsx
import React from "react";
import { Post } from "./model/posts";
// `Post` 타입에서 `postId`만 선택한 새로운 타입을 정의
export type PostsProps = {
postInfo: Pick<Post, "postId">[]; // postId만 사용한 배열
};
const PostsPage: React.FC<PostsProps> = ({ postInfo }) => {
return (
<ul>
{postInfo.map((post) => (
<li key={post.postId}>
<p>Post ID: {post.postId}</p>
</li>
))}
</ul>
);
};
export default PostsPage;
Pick을 사용한 useState로 선택된 속성 관리
editTodo는 선택된 두 가지 속성(title, content)만 포함하는 상태로 관리되기 때문에, 편집 기능을 구현할 때 유용하다.
// type/todo.type.ts
export type EditTodo = Pick<Todo, "title" | "content">;
// TodoItem.tsx
const [editTodo, setEditTodo] = useState<EditTodo | null>(null);
8. api에서 TS 사용하기
API 응답 타입을 제네릭으로 정의한다.
제네릭을 사용하면 다양한 타입에 대해 재사용 가능한 코드를 작성할 수 있다.
// src/model/api.ts
export type ApiResponse<T> = {
data: T[];
totalPage: number;
page: number;
};
T
는 제네릭 타입 매개변수이다. 이 타입은 나중에 특정 타입으로 지정된다.data: T[]
: API에서 응답받는 데이터는 항상 배열 형태이다. 이 배열의 요소는 T 타입이다. 즉, 다양한 타입의 데이터를 받을 수 있다.
아래와 같이 구체적인 타입 정의가 가능하다.
PostResponse: 이 타입은 PostResponse
의 T
를 Post
로 설정하여, data
필드가 Post
객체의 배열이 되도록 한다.
// src/model/posts.ts
import { ApiResponse } from "./api";
export interface Post {
postId: number;
title: string;
body: string;
date: string;
tags: string[];
user: User;
}
export interface User {
userId: number;
username: string;
}
// 구체적인 타입 정의
export type PostResponse = ApiResponse<Post>; // Post 객체의 배열
이제 PostResponse를 API 서비스에서 사용하여 응답 타입을 정의할 수 있다.
// src/api/posts.ts
import axios from "axios";
import { Post, PostResponse } from "../model/posts";
const API_URL = "https://example.com/api/posts";
// API에서 게시물을 가져오는 함수
export const fetchPosts = async (): Promise<PostResponse> => {
const response = await axios.get<PostResponse>(API_URL);
return response.data;
};
마지막으로 이제
App.tsx
에서fetchPosts
를 사용하여 데이터를 가져오면 된다.
// src/App.tsx
import React, { useEffect, useState } from "react";
import PostsPage from "./PostsPage";
import { Post } from "./model/posts";
import UserPage from "./UserPage";
import { fetchPosts } from "./api/posts"; // import
const App: React.FC = () => {
const [myPosts, setMyPosts] = useState<Post[]>([]);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const loadPosts = async () => {
try {
const posts = await fetchPosts();
setMyPosts(posts.data); // posts.data로 실제 게시물 배열 접근
} catch (err) {
setError("게시물을 로드할 수 없습니다.");
}
};
loadPosts();
}, []);
const changeTitle = (id: number, newTitle: string) => {
const updatedPosts = myPosts.map((post) =>
post.postId === id ? { ...post, title: newTitle } : post
);
setMyPosts(updatedPosts);
};
const showUserName = (userName: string) => {
return userName;
};
if (error) {
return <div>{error}</div>; // 에러 메시지 표시
}
return (
<div>
<PostsPage postInfo={myPosts} />
<UserPage
username="시은"
email="sieun@example.com"
isActive={true}
showUserName={showUserName}
/>
</div>
);
};
export default App;
댓글남기기