프로젝트 준비
먼저, 필요한 라이브러리 및 프로젝트 생성부터 시작해본다.
npx create-react-app todo-app --template typescript
cd todo-app
npm install classnames react-icons sass
classnames라이브러리는 조건부 스타일링을 조금 더 편리하게 할 수 있도록 만들어주고, react-icons는 리액트에서 다양한 아이콘을 사용할 수 있는 라이브러리이다. 아이콘의 크기나 색상은 props 혹은 CSS 스타일로 변경해서 사용할 수 있다.
그리고, 여백의 공간을 회색으로 변경해주기 위해 index.css를 약간 수정해준다.
/* index.css */
body {
margin: 0;
padding: 0;
background: #e0ecef;
}
UI 구성
앞으로 만들 컴포넌트의 종류와 역할을 소개한다.
- TodoTemplate : 화면을 가운데로 정렬시켜주며, 앱 타이틀(일정 관리)를 보여준다. children으로 내부 JSX를 props로 받아와서 렌더링해준다.
- TodoInsert: 새로운 항목을 입력하고 추가할 수 있는 컴포넌트이다. state를 통해 인풋의 상태를 관리한다.
- TodoListItem: 각 할 일 항목에 대한 정보를 보여 주는 컴포넌트이다. todo 객체를 props로 받아와서 상태에 따라 다른 스타일의 UI를 보여준다.
- TodoList: todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoListItem 컴포넌트로 변환해서 보여준다.
TodoTemplate
TodoTemplate을 먼저 작성해준다.
// TodoTemplate.tsx
interface Props {
children: string;
}
export default function TodoTemplate(props: Props) {
return (
<div className="TodoTemplate">
<div className="app-title">일정 관리</div>
<div className="content">{props.children}</div>
</div>
);
}
// App.tsx
import "./App.css";
import TodoTemplate from "./TodoTemplate";
function App() {
return <TodoTemplate>Todo-App</TodoTemplate>;
}
export default App;
위의 코드만 렌더링해보면 다음과 같은 결과가 나온다.
이 코드에 스타일을 입혀주어 어떻게 변하는지 확인해보자.
/* TodoTemplate.scss */
.TodoTemplate {
width: 512px;
margin-left: auto;
margin-right: auto;
margin-top: 6rem;
border-radius: 4px;
overflow: hidden;
.app-title {
background: pink;
color: white;
height: 4rem;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.content {
background: white;
}
}
위의 css 코드를 살펴보면 다음과 같다.
- width : 컨테이너의 너비를 512 픽셀로 설정한다.
- margin-left, margin-right : 자동으로 좌우 마진을 설정하여 컨테이너를 수평 중앙에 위치시킨다.
- margin-top : 컨테이너의 상단 마진을 6rem으로 설정한다.
- border-radius : 컨테이너의 모서리를 둥글게 처리한다.
- overflow : 컨테이너의 내용이 넘칠 경우, 스크롤바를 표시해 스크롤이 가능하도록 설정한다.
- .app-title : 컨테이너의 상단에 위치한 타이틀 영역을 스타일링한다. 배경색은 분홍색, 글자 색은 흰색, 높이는 4rem, 글자 크기는 1.5rem, 텍스트를 수직, 수평 중앙에 위치시키도록 설정한다.
- .content : 컨테이너의 내용 영역을 스타일링한다. 배경색을 흰색으로 설정한다.
여기서 rem은 "root em"의 약자로, 상위 요소의 폰트 크기에 상관 없이 고정된 크기를 나타내는 단위이다. 즉, rem은 HTML 문서의 루트 요소의 기본 폰트 크기를 기준으로 크기를 결정한다.
예를 들어, HTML 문서에서 루트 요소의 기본 폰트 크기가 16px라면, 1rem은 16px를 의미하고, 2rem은 32px를 의미한다.
rem을 사용하면 페이지의 전체적인 비율이 일정하게 유지되기 때문에, 반응형 웹 디자인에서 유용하게 사용된다.
TodoInsert
// TodoInsert.tsx
import { MdAdd } from "react-icons/md";
export default function TodoInsert() {
return (
<form className="TodoInsert">
<input placeholder="할 일을 입력하세요" />
<button type="submit">
<MdAdd />
</button>
</form>
);
}
// App.tsx
import "./App.css";
import TodoTemplate from "./TodoTemplate";
import TodoInsert from "./TodoInsert";
function App() {
return (
<TodoTemplate>
<TodoInsert />
</TodoTemplate>
);
}
export default App;
위 코드를 작성한 뒤, 수정사항으로 TodoTemplate의 Props를 수정해준다. string으로 되어있어 TodoInsert를 children으로 받지 못할것이다.
// TodoTemplate.tsx
interface Props {
children: React.ReactNode;
}
그 뒤, 결과를 렌더링해보면 다음과 같이 나온다.
이제, 이 컴포넌트를 스타일링 해본다.
/* TodoInsert.scss */
.TodoInsert {
display: flex;
background: #495057;
input {
background: none;
outline: none;
border: none;
padding: 0.5rem;
font-size: 1.125rem;
line-height: 1.5;
color: white;
&::placeholder {
color: #dee2e6;
}
flex: 1;
}
button {
background: none;
outline: none;
border: none;
background: #868e96;
color: white;
padding-left: 1rem;
padding-right: 1rem;
font-size: 1.5rem;
display: flex;
align-items: center;
cursor: pointer;
transition: 0.1s background ease-in;
&:hover {
background: #adb5bd;
}
}
}
이 코드는 CSS 모듈을 사용하여 TodoInsert 컴포넌트의 스타일을 지정한다.
TodoInsert 컴포넌트는 입력 필드와 추가 버튼으로 이루어져 있으며, display 속성을 flex로 설정하여 가로 방향으로 배치한다. 배경색은 #495057으로 지정한다.
input 태그에는 배경색을 없애고, 아웃라인과 테두리를 없앤 후, 패딩과 폰트 크기, 줄 높이 등을 설정해준다. placeholder에는 색상을 #dee2e6으로 지정합니다. flex 속성을 1로 설정하여, 입력 필드가 가능한 최대 크기를 차지하도록 합니다.
placeholder는 input 요소나 textarea 요소 등의 폼 요소에서 입력 필드가 비어 있을 때, 해당 입력 필드에 표시되는 힌트나 안내 메세지를 지정하는 속성이다. 이란적으로 입력 필드가 무엇을 의미하는지 알려주거나, 입력 형식이 어떤 형태여야 하는지 알려주는 등의 역할을 한다. 사용자가 입력을 시작하면, placeholder는 자동으로 사라진다.
button 태그에는 배경색과 아웃라인, 테두리를 없앤 후, 배경색을 #868e96으로, 글자색을 흰색으로 지정한다. 패딩과 폰트 크기를 설정하고, display 속성을 flex로 설정하여 내부 요소를 수평 정렬한다. 커서를 포인터로 설정하여 버튼을 누를 수 있도록 설정한다. transition 속성을 사용하여 배경색 변경 시 부드러운 애니메이션 효과를 주고, hover pseudo-class를 사용하여 마우스를 올리면 배경색이 #adb5bd로 변경된다.
TodoListItem, TodoList
// TodoListItem.tsx
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from "react-icons/md";
export default function TodoListItem() {
return (
<div className="TodoListItem">
<div className="checkbox">
<MdCheckBoxOutlineBlank />
<div className="text">할 일</div>
</div>
<div className="remove">
<MdRemoveCircleOutline />
</div>
</div>
);
}
// TodoList.tsx
import TodoListItem from "./TodoListItem";
export default function TodoList() {
return (
<div className="TodoList">
<TodoListItem />
<TodoListItem />
<TodoListItem />
</div>
);
}
// App.tsx
import "./App.css";
import TodoTemplate from "./TodoTemplate";
import TodoInsert from "./TodoInsert";
import TodoList from "./TodoList";
function App() {
return (
<TodoTemplate>
<TodoInsert />
<TodoList />
</TodoTemplate>
);
}
export default App;
TodoListItem과 TodoList를 만들어주었다. TodoListItem 컴포넌트는 각각의 할 일 아이템을 나타내며, 체크박스, 내용, 삭제 버튼으로 구성된다. react-icons에서 가져온 아이콘 컴포넌트를 활용해주었다.
TodoList는 TodoListItem 컴포넌트를 세 번 렌더링해서 표시해준다.
그리고나서 스타일을 적용해주었다.
/* TodoList.scss */
.TodoList {
min-height: 320px;
max-height: 513px;
overflow-y: auto;
}
- min-height : 최소 높이를 나타낸다. TodoList에 할 일이 없을 경우에도 TodoList 영역이 화면에서 사라지지 않도록 최소 높이를 지정해준다.
- max-height : 최대 높이를 나타낸다. TodoList에 할 일이 많을 경우에도 TodoList 영역이 저장한 최대 높이를 넘지 않도록 지정해준다.
- overflow-y : 세로축 방향으로 스크롤바가 표시되도록 설정한다. auto로 지정하면 내용이 컨테이너를 벗어나면 스크롤바가 자동으로 표시된다. 따라서 TodoList가 스크롤 가능한 컨테이너 내에 나타나게 된다.
/* TodoListItem.scss */
.TodoListItem {
padding: 1rem;
display: flex;
align-items: center;
&:nth-child(even) {
background: #f8f9fa;
}
.checkbox {
cursor: pointer;
flex: 1;
display: flex;
align-items: center;
svg {
font-size: 1.5rem;
}
.text {
margin-left: 0.5rem;
flex: 1;
}
&.checked {
svg {
color: #22b8cf;
}
.text {
color: #adb5bd;
text-decoration: line-through;
}
}
}
.remove {
display: flex;
align-items: center;
font-size: 1.5rem;
color: #ff6b6b;
cursor: pointer;
&:hover {
color: #ff6b6b;
}
}
& + & {
border-top: 1px solid #dee2e6;
}
}
- padding : 컨텐츠와 경계 사이의 간격을 지정한다. 여기에서는 할 일 아이템의 내용과 경계 사이에 1rem 간격을 둔다.
- display : flex , align-item : center 는 할 일 아이템의 내용과 체크박스 및 삭제 버튼을 수평으로 정렬한다.
- &:nth-child(even)는 짝수번째 할 일 아이템에 대해 배경색을 설정한다.
- .checkbox는 체크박스와 할 일 내용을 담는 영역이다.
- & + & : 이전 할 일 아이템과 현재 할 일 아이템 사이에 경계선을 그린다.
기능 구현
App 컴포넌트에서 상태 설정
나중에 추가할 일정 항목에 대한 상태들은 모두 App컴포넌트에서 관리한다. App에서 useState를 사용해 todos라는 상태를 정의하고, todos를 TodoList의 props로 전달해보자.
// App.tsx
import "./App.css";
import TodoTemplate from "./TodoTemplate";
import TodoInsert from "./TodoInsert";
import TodoList from "./TodoList";
import { useState } from "react";
function App() {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 알아보기",
checked: true,
},
{
id: 2,
text: "컴포넌트 스타일 적용",
checked: true,
},
{
id: 3,
text: "일정 관리 앱 만들기",
checked: false,
},
]);
return (
<TodoTemplate>
<TodoInsert />
<TodoList todos={todos} />
</TodoTemplate>
);
}
export default App;
useState 함수를 사용해서 todos state값을 초기화하고 있다. todos는 배열이고, 각 요소는 객체 형태로 {id, text, checked} 프로퍼티를 가진다. id는 고유한 식별자, text는 일정의 내용, checked는 일정이 완료되었는지 여부를 나타낸다.
TodoList에 todos 배열을 props로 전달해서 각각의 일정을 보여줄 수 있다.
// TodoList.tsx
import TodoListItem from "./TodoListItem";
import "./TodoList.scss";
interface Todo {
id: number;
text: string;
checked: boolean;
}
interface Props {
todos: Todo[];
}
export default function TodoList({ todos }: Props) {
return (
<div className="TodoList">
{todos.map((todo) => (
<TodoListItem todo={todo} key={todo.id} />
))}
</div>
);
}
이 코드는 TodoList 컴포넌트를 정의하는 코드이다. 컴포넌트는 interface를 이용해서 Props 타입을 정의했다. Props 타입에는 Todo 타입의 배열인 todos가 있다.
TodoList 컴포넌트는 todos 배열을 map 함수를 사용해 반복하면서 TodoListItem 컴포넌트를 렌더링한다. 각 TodoListItem 에는 todo 객체를 전달하며, 이 객체는 text, checked, id 프로퍼티를 가지고 있다. key prop은 각 TodoListItem이 유일한 값을 가지도록 하기위해 사용된다.
// TodoListItem.tsx
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from "react-icons/md";
import "./TodoListItem.scss";
import cn from "classnames";
interface Todo {
id: number;
text: string;
checked: boolean;
}
interface Props {
todo: Todo;
}
export default function TodoListItem({ todo }: Props) {
const { text, checked } = todo;
return (
<div className="TodoListItem">
<div className={cn("checkbox", { checked })}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove">
<MdRemoveCircleOutline />
</div>
</div>
);
}
TodoListItem 컴포넌트는 Props 인터페이스를 통해 todo 프로퍼티를 받아오게 된다. TodoListItem 컴포넌트는 Todo 객체에 따라서 렌더링된다. Todo 객체의 text와 checked 값이 렌더링되고, checked 값에 따라서 아이콘의 모양이 바뀐다. cn 함수는 classnames 라이브러리의 함수로, 여러 개의 클래스 이름을 조건에 따라서 동적으로 설정할 수 있다.
항목 추가 기능 구현
항목 추가 기능은 TodoInsert 컴포넌트에서 인풋 상태를 관리하고 App 컴포넌트에서는 todos 배열에 새로운 객체를 추가하는 함수를 만들어줘야 한다.
// TodoInsert.tsx
import { useCallback, useState } from "react";
import { MdAdd } from "react-icons/md";
import "./TodoInsert.scss";
export default function TodoInsert() {
const [value, setValue] = useState("");
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}, []);
return (
<form className="TodoInsert">
<input
placeholder="할 일을 입력하세요"
value={value}
onChange={onChange}
/>
<button type="submit">
<MdAdd />
</button>
</form>
);
}
그리고, App 컴포넌트에서 todos 배열에 새 객체를 추가하는 onInsert 함수를 만들어주어야 한다.
import "./App.css";
import TodoTemplate from "./TodoTemplate";
import TodoInsert from "./TodoInsert";
import TodoList from "./TodoList";
import { useCallback, useRef, useState } from "react";
export default function App() {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 알아보기",
checked: true,
},
{
id: 2,
text: "컴포넌트 스타일 적용",
checked: true,
},
{
id: 3,
text: "일정 관리 앱 만들기",
checked: false,
},
]);
const nextId = useRef(4);
const onInsert = useCallback(
(text: string) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo));
nextId.current += 1;
},
[todos]
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} />
</TodoTemplate>
);
}
이 함수에서는 새로운 객체를 만들때마다 id값에 1을 더해주고, 이 값은 렌더링되는 정보가 아니기 때문에 useRef를 사용해 컴포넌트에서 관리한다. 이 값은 화면에 보이지도 않고 리렌더링될 필요도 없기 때문이다.
또한, onInsert 함수는 TodoInsert 컴포넌트의 props로 설정해주었다.
이제부터는 버튼을 클릭하면 발생하는 이벤트를 설정해보자. App에서 TodoInsert에 넣어 준 onInsert 함수에 현재 useState를 통해 관리하고 있는 value 값을 파라미터로 넣어서 호출한다.
// TodoInsert.tsx
import { useCallback, useState } from "react";
import { MdAdd } from "react-icons/md";
import "./TodoInsert.scss";
interface TodoInsertProps {
onInsert: (text: string) => void;
}
export default function TodoInsert({ onInsert }: TodoInsertProps) {
const [value, setValue] = useState("");
const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}, []);
const onSubmit = useCallback(
(e: React.FormEvent<HTMLFormElement>) => {
onInsert(value);
setValue("");
e.preventDefault();
},
[onInsert, value]
);
return (
<form className="TodoInsert" onSubmit={onSubmit}>
<input
placeholder="할 일을 입력하세요"
value={value}
onChange={onChange}
/>
<button type="submit">
<MdAdd />
</button>
</form>
);
}
onSubmit이라는 함수를 만들고, 이를 form의 onSubmit으로 설정했다. 이 함수가 호출되면 props로 받아 온 onInsert 함수에 현재 value 값을 파라미터로 넣어서 호출하고, 현재 value값을 초기화한다.
추가로, onSubmit 함수는 브라우저를 새로고침시킨다. 이때 e.preventDefault()함수를 호출하면 새로고침을 방지할 수 있다.
여기까지 했을 때의 렌더링된 화면을 보자.
일정을 직접 추가할 수 있게 되었다.
지우기 기능 구현
지우기 기능을 구현하는데 있어, 리액트에서는 배열의 불변성을 지키면서 배열 원소를 제거해야할 경우, 배열 내장 함수인 filter를 사용하면 매우 간편하게 구현할 수 있다.
filter 함수를 사용해서 onRemove 함수를 작성해보자. App 컴포넌트에 id를 파라미터로 받아와서 같은 id를 가진 항목을 todos 배열에서 지우는 함수이다. 이 함수는 TodoList의 props로 설정해준다.
// App.tsx
import "./App.css";
import TodoTemplate from "./TodoTemplate";
import TodoInsert from "./TodoInsert";
import TodoList from "./TodoList";
import { useCallback, useRef, useState } from "react";
export default function App() {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 알아보기",
checked: true,
},
{
id: 2,
text: "컴포넌트 스타일 적용",
checked: true,
},
{
id: 3,
text: "일정 관리 앱 만들기",
checked: false,
},
]);
const nextId = useRef(4);
const onInsert = useCallback(
(text: string) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo));
nextId.current += 1;
},
[todos]
);
const onRemove = useCallback(
(id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos]
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} />
</TodoTemplate>
);
}
TodoListItem 컴포넌트에서 onRemove 함수를 사용하려면 TodoList 컴포넌트를 거쳐야한다. 따라서 App에서 받아온 Props를 TodoList에서 그대로 TodoListItem으로 넘겨준다.
// TodoList.tsx
import "./App.css";
import TodoTemplate from "./TodoTemplate";
import TodoInsert from "./TodoInsert";
import TodoList from "./TodoList";
import { useCallback, useRef, useState } from "react";
export default function App() {
const [todos, setTodos] = useState([
{
id: 1,
text: "리액트 기초 알아보기",
checked: true,
},
{
id: 2,
text: "컴포넌트 스타일 적용",
checked: true,
},
{
id: 3,
text: "일정 관리 앱 만들기",
checked: false,
},
]);
const nextId = useRef(4);
const onInsert = useCallback(
(text: string) => {
const todo = {
id: nextId.current,
text,
checked: false,
};
setTodos(todos.concat(todo));
nextId.current += 1;
},
[todos]
);
const onRemove = useCallback(
(id: number) => {
setTodos(todos.filter((todo) => todo.id !== id));
},
[todos]
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} />
</TodoTemplate>
);
}
// TodoListItem.tsx
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from "react-icons/md";
import "./TodoListItem.scss";
import cn from "classnames";
interface Todo {
id: number;
text: string;
checked: boolean;
}
interface Props {
todo: Todo;
onRemove: (id: number) => void;
}
export default function TodoListItem({ todo, onRemove }: Props) {
const { id, text, checked } = todo;
return (
<div className="TodoListItem">
<div className={cn("checkbox", { checked })}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
);
}
TodoListItem 컴포넌트에서는 remove 버튼에 onClick으로 onRemove 함수가 실행되도록 해줬다.
그리고나서 다시 렌더링해보면
항목을 삭제할 수 있다.
수정 기능
수정 기능은 삭제 기능과 비슷하다. onToggle이라는 함수를 App 컴포넌트에 만들어주고, 해당 함수를 TodoList 컴포넌트에 props로 전달해준다. 그 다음 TodoList를 통해 TodoListItem으로 전달해준다.
export default function App() {
// (...App)
const onToggle = useCallback(
(id: number) => {
setTodos(
todos.map((todo) =>
todo.id === id ? { ...todo, checked: !todo.checked } : todo
)
);
},
[todos]
);
return (
<TodoTemplate>
<TodoInsert onInsert={onInsert} />
<TodoList todos={todos} onRemove={onRemove} onToggle={onToggle} />
</TodoTemplate>
);
}
위 코드에서는 배열 내장 함수 map을 사용해 특정 id를 가지고 있는 객체의 checked 값을 반전시켜주었다. 불변성을 유지하면서 특정 배열 원소를 업데이트 해야할 때 이렇게 map을 사용하면 짧은 코드로 쉽게 작성할 수 있다.
onToggle 함수에서는 삼항연산자가 사용되었다. todo.id와 현재 파라미터로 사용된 id 값이 같을 때는 새로운 객체를 생성하지만, id값이 다를 경우에는 변화를 주지 않고 처음 받아왔던 상태 그대로 반환한다. 그렇기 때문에 map을 사용해 만든 배열에서 변화가 필요한 원소만 업데이트되고 나머지는 그대로 남아있게 된다.
그리고 TodoListItem에서 만들어준 토글 함수를 호출해준다.
// TodoList.tsx
import TodoListItem from "./TodoListItem";
import "./TodoList.scss";
interface Todo {
id: number;
text: string;
checked: boolean;
}
interface Props {
todos: Todo[];
onRemove: (id: number) => void;
onToggle: (id: number) => void;
}
export default function TodoList({ todos, onRemove, onToggle }: Props) {
return (
<div className="TodoList">
{todos.map((todo) => (
<TodoListItem
todo={todo}
key={todo.id}
onRemove={onRemove}
onToggle={onToggle}
/>
))}
</div>
);
}
// TodoListItem.tsx
import {
MdCheckBoxOutlineBlank,
MdCheckBox,
MdRemoveCircleOutline,
} from "react-icons/md";
import "./TodoListItem.scss";
import cn from "classnames";
interface Todo {
id: number;
text: string;
checked: boolean;
}
interface Props {
todo: Todo;
onRemove: (id: number) => void;
onToggle: (id: number) => void;
}
export default function TodoListItem({ todo, onRemove, onToggle }: Props) {
const { id, text, checked } = todo;
return (
<div className="TodoListItem">
<div className={cn("checkbox", { checked })} onClick={() => onToggle(id)}>
{checked ? <MdCheckBox /> : <MdCheckBoxOutlineBlank />}
<div className="text">{text}</div>
</div>
<div className="remove" onClick={() => onRemove(id)}>
<MdRemoveCircleOutline />
</div>
</div>
);
}
리렌더링 해보자.
'WEB > React' 카테고리의 다른 글
리액트 불변성 유지하기 [react/immer/typescript] (0) | 2023.03.18 |
---|---|
리액트 컴포넌트 성능 최적화 [react/component/typescript] (0) | 2023.03.18 |
리액트 CSS 사용하기 [react/css/typescript] (0) | 2023.03.17 |
리액트 Hooks 이해하기 [react/hooks/typescript] (0) | 2023.03.15 |
리액트 라이프사이클 메서드 사용하기 [react/life cycle/typescript] (0) | 2023.03.13 |