타입스크립트 핸드북과 Udemy강의를 토대로 Typescript를 공부합니다.
프로젝트 목적 : 할 일 관리하는(to-do) 앱 구현하기
이 프로젝트를 통해서...
타입스크립트와 리액트, 그리고 Lodash같은 써드파티 라이브러리를 사용하게 된다.
리액트 개발 프로젝트들에서 타입스크립트를 자주 사용하지는 않지만 가능하다.
리액트 앱을 타입스크립트에서 어떻게 개발하는지 배울 수 있다.
React + Typescript 프로젝트 설정하기 (공식문서)
cra-template-typescript 패키지가 없어서 js파일로 만들어질 수 있으므로 미리 설치
1. npm install cra-template-typescript -g
2. npx create-react-app my-app --template typescript
에러가 난다면?
package.json에서 typescript : 현재 typescript버전으로 바꿔야 한다.
4보다 낮으면 최신버전으로 새로 깔아야 한다.
이렇게 안하면 에러 생김! 항상 최신 typescript로 작업하는 것이 좋다.
타입스크립트를 지원하는 리액트 프로젝트를 시작할 수 있다.
일반적 리액트 앱과 같지만 타입스크립트 코드를 쓸 수 있고,
tsconfig.json파일이 생성되어 있다.
그리고 src폴더 안에 App.tsx파일이 생성되어 있다.
(원래 리액트 앱의 src폴더 안에는 APP.js가 생성)
.tsx 파일들이 사용되는 이유는,
여기 타입스크립트만 쓸 수 있는 것이 아니라, jsx 코드도 쓸 수 있다.
타입스크립트안에 자바스크립트 코드를 쓸 수 있는 특별한 리액트 자바스크립트의 문법 구조!
초기 App.tsx파일
타입배정 등 이미 타입스크립트 문법구조를 볼 수 있다.
//App.tsx
import React from 'react';
const App: React.FC = () => {
return (
<div className="App">
</div>
);
};
export default App;
Index.tsx
//index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
index.css
html {
font-family: sans-serif;
}
body {
margin: 0;
}
App은 화살표 함수인데 타입을 function이라고 하지 않는 이유?
const App: React.FC = () => {
return (
<div className="App">
</div>
);
};
React.FC는 함수타입과 같지만, 리액트에서 제공하는 타입으로 리액트 타입 패키지에서 제공하는 것이다.
React.FC은 node modules의 많은 리액트 타입을 가지는 @types폴더에서 자동으로 가져오기가 된 것이다.
FC타입 : function component의 약자, 평범한 함수가 아닌 함수컴포넌트 역할
React.FC === React.FunctionComponent
화살표함수가 JSX를 리턴하기 때문에 React.FC 타입을 사용한다.
TodoList컴포넌트 만들고 props로 todos내려주기
//App.tsx
import React from 'react';
import TodoList from './components/TodoList';
const App: React.FC = () => {
const todos = [{ id: 't1', text: 'Finish the course' }];
//원래 TodoList.jsx에 있던 todos배열을 App.jsx로 옮기고 props로 전달해준다.
return (
<div className="App">
{/* A component that adds todos */}
<TodoList items={todos} />
</div>
);
};
//items 에러! : item이라는 성질이 이 타입에 존재하지 않는다.
//props로 내려줄 todos의 props키 items의 타입을 정의해야 한다!(TodoList.jsx파일에서)
export default App;
//TodoList.tsx
import React from 'react';
//App.tsx에서 props의 키인 items의 타입을 정의하기 위해 인터페이스 사용
interface TodoListProps {
items: {id: string, text: string}[];
};
//문제 : 타입스크립트는 props에 item키가 있다는 것을 모른다.
//해결 : TodoList컴포넌트를 제네릭타입으로 정의한다.
//제네릭타입은 props의 구조(객체)를 따라서 만들면 된다.
//코드의 깔끔함을 위해 인터페이스로 타입을 만든다.
//props객체의 items키에 접근하여 todo를 가진 todo배열을 map으로 li태그에 감싸서 todo.text를 렌더링한다.
const TodoList: React.FC<TodoListProps> = props => {
return (
<ul>
{props.items.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
};
export default TodoList;
할일 추가하기
NewTodo 컴포넌트 새로 만들기
//NewTodo.tsx
import React, { useRef } from 'react';
//NewTodo는 todo(할일)를 추가하는 컴포넌트
//input태그는 할일을 적는 태그
const NewTodo: React.FC = () => {
//ref를 사용하면 제출될때 유저가 입력한 내용을 추출할 수 있다.
//ref를 사용하려면 react hook중 하나인 useRef를 import해야한다.
//input과 연결시켜 입력값을 받아온다.
//ref에 오류가 뜨는데, 이는 ref 안에 어떤 데이터가 저장될지 모르므로 타입을 설정해야 한다.
//inputelement이므로 HTMLInputElement가 저장될 것이다.
//그리고 ref가 만들어질때 기본값이 필요하므로 null로 저장
const textInputRef = useRef<HTMLInputElement>(null);
//form이 제출될때마다 실행
//event타입이 그냥 Event가 아닌 React.FormEvent를 써야 한다.
const todoSubmitHandler = (event: React.FormEvent) => {
event.preventDefault();
const enteredText = textInputRef.current!.value;
//textInputRef.current의 초기값을 null로 설정해서 null이 온다고 타입스크립트가 생각하는데,
//우리는 null이 아닌 값이 올것을 알기에 !로 이 값이 존재함을 알린다.
//ref를 사용할때는 current를 사용해야 한다.
console.log(enteredText);
}
//실행하는 것이 아니라 props로 내려주는 역할
return <form onSubmit={todoSubmitHandler}>
<div>
<label htmlFor="todo-text">Todo Text</label>
<input type="text" id="todo-text" ref={textInputRef}/>
</div>
<button type="submit">ADD TODO</button>
</form>
};
export default NewTodo;
//App.tsx
import React from 'react';
import NewTodo from './components/NewTodo';
import TodoList from './components/TodoList';
const App: React.FC = () => {
const todos = [{ id: 't1', text: 'Finish the course' }];
return (
<div className="App">
<NewTodo />
<TodoList items={todos} />
</div>
);
};
export default App;
NewTodo컴포넌트의 input창에서 입력된 텍스트를 App컴포넌트로 옮기기
//App.tsx
import React from 'react';
import NewTodo from './components/NewTodo';
import TodoList from './components/TodoList';
const App: React.FC = () => {
const todos = [{ id: 't1', text: 'Finish the course' }];
//NewTodo에서 받아온 todo를 todos배열에 넣는 함수
//props로 NewTodo컴포넌트로 이 함수를 내려준다.
const todoAddHandler = (text: string) => {
console.log(text);
//text는 NewTodo컴포넌트의 enteredText가 되고 input입력값이 콘솔에 찍힌다.
};
return (
<div className="App">
<NewTodo onAddTodo={todoAddHandler}/>
<TodoList items={todos} />
</div>
);
};
export default App;
//NewTodo.tsx
import React, { useRef } from 'react';
//TodoList에서는 props타입설정을 인터페이스로 했지만, 여기서는 type로 설정한다.
type NewTodoProps = { //NewTodoProps의 타입은 객체
onAddTodo : (todoText: string) => void;
//props키인 onAddTodo는 아무것도 반환하지 않는 함수 타입!
//단, 문자열인 하나의 매개변수를 기대한다.
};
const NewTodo: React.FC<NewTodoProps> = props => {
const textInputRef = useRef<HTMLInputElement>(null);
const todoSubmitHandler = (event: React.FormEvent) => {
event.preventDefault();
const enteredText = textInputRef.current!.value;
props.onAddTodo(enteredText);
//props가 onAddTodo키를 가지고 있는지 타입스크립트는 모르므로 props타입을 설정해줘야 한다.
////onAddTodo함수에 enteredText를 전달인자로 넣어서 함수를 호출한다.
}
return <form onSubmit={todoSubmitHandler}>
<div>
<label htmlFor="todo-text">Todo Text</label>
<input type="text" id="todo-text" ref={textInputRef}/>
</div>
<button type="submit">ADD TODO</button>
</form>
};
export default NewTodo;
Todo를 state로 렌더링하기
Todo를 업데이트할때마다 TodoList컴포넌트 렌더링하기
//App.tsx
import React, {useState} from 'react';
import NewTodo from './components/NewTodo';
import TodoList from './components/TodoList';
import { Todo } from './todo.model';
const App: React.FC = () => {
//const todos = [{ id: 't1', text: 'Finish the course' }];
//todos의 상태관리
//초기값을 빈배열로 둔다면 타입스크립트는 state가 항상 빈배열인 줄 알기 때문에
//useState 초기값의 타입설정을 해야한다.
//객체를 요소로 둔 배열로
//const [todos, setTodos] = useState<{id:string, text:string}[]>([]);
const [todos, setTodos] = useState<Todo[]>([]);//인터페이스로 타입을 띠로 빼줌
const todoAddHandler = (text: string) => {
//console.log(text);
//setTodos([...todos, {id: Math.random().toString(), text: text}]);
//기존의 todos에 새로 추가할 todo를 넣어주면 되는데,
//기존의 todos가 최신상태가 아닐 수도 있다.
//해결 : 상태 업데이트하는 함수에 함수를 전달한다.
//이 함수는 이전의 todos를 불러오고 새로운 상태를 리턴한다.
setTodos(prevTodos => [...prevTodos, {id: Math.random().toString(), text: text}]);
};
return (
<div className="App">
<NewTodo onAddTodo={todoAddHandler}/>
<TodoList items={todos} />
</div>
);
};
export default App;
//todo.model.ts
//Todo item이 어떤 구조를 가지고 있는지 설명하는 인터페이스
//export로 앱의 여러위치에 쓰일 수 있게한다.
export interface Todo{
id: string;
text: string;
}
todos 삭제하기 기능 추가
//App.tsx
import React, { useState } from 'react';
import NewTodo from './components/NewTodo';
import TodoList from './components/TodoList';
import { Todo } from './todo.model';
const App: React.FC = () => {
//const todos = [{ id: 't1', text: 'Finish the course' }];
//const [todos, setTodos] = useState<{id:string, text:string}[]>([]);
const [todos, setTodos] = useState<Todo[]>([]);
const todoAddHandler = (text: string) => {
//console.log(text);
//setTodos([...todos, {id: Math.random().toString(), text: text}]);
setTodos(prevTodos => [...prevTodos, {id: Math.random().toString(), text: text}]);
};
//todos중 매개변수로 받은 todos.id를 가진 item을 삭제하는 이벤트핸들러
//TodoList컴포넌트에 props로 내려준다.
const todoDeleteHandler = (todoId: string) => {
setTodos(prevTodos => {
return prevTodos.filter(todo => todo.id !== todoId);
});
//prevTodos함수 : 기존의 todos를 가져오는 함수
};
return (
<div className="App">
<NewTodo onAddTodo={todoAddHandler}/>
<TodoList items={todos} onDeleteTodo={todoDeleteHandler}/>
</div>
);
};
export default App;
//TodoList.tsx
import React from 'react';
interface TodoListProps {
items: {id: string, text: string}[];
onDeleteTodo: (id: string) => void;
//props의 키 onDeleteTodo는 아무것도 반환하지 않는 함수지만,
//매개변수로 문자열타입의 id를 받는다.
};
const TodoList: React.FC<TodoListProps> = props => {
return (
<ul>
{props.items.map(todo => (
<li key={todo.id}>
<span>{todo.text}</span>
<button onClick={props.onDeleteTodo.bind(null, todo.id)}>DELETE</button>
</li>
))}
</ul>
);
};
//onClick={props.onDeleteTodo} 에러!
//bind를 사용해서 첫번째 매개변수는 가르킬 곳이 없으므로 this생략으로 null
//두번째 매개변수는 onDeleteTodo가 받을 첫번째 매개변수
//Todo.id가 되어야 한다.
export default TodoList;
//TodoList.css
ul {
list-style: none;
width: 90%;
max-width: 40rem;
margin: 2rem auto;
padding: 0;
}
li {
margin: 1rem 0;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.26);
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
//NewTodo.css
form {
width: 90%;
max-width: 40rem;
margin: 2rem auto;
}
.form-control {
margin-bottom: 1rem;
}
label, input {
display: block;
width: 100%;
}
label {
font-weight: bold;
}
input {
font: inherit;
border: 1px solid #ccc;
padding: 0.25rem;
}
input:focus {
outline: none;
border-color: #50005a;
}
button {
background: #50005a;
border: 1px solid #50005a;
color: white;
padding: 0.5rem 1.5rem;
cursor: pointer;
}
button:focus {
outline: none;
}
button:hover,
button:active {
background: #6a0a77;
border-color: #6a0a77;
}
//App.tsx
const App: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>(() =>
JSON.parse(window.localStorage.getItem("todos")!) || []);
//초기값이 빈배열에서 -> 로컬 스토리지에 저장해둔 todos 상태값이 있디면 꺼내쓸 수 있도록 변경
useEffect(() => {
window.localStorage.setItem("todos", JSON.stringify(todos));
}, [todos]);
//상태값이 변경될 때 마다 로컬 스토리지에 해당값이 저장되도록 useEffect() 호출
...
};
이번 프로젝트로 시도 해봐야 할 것
1. react hook중 useEffect를 사용해서 리팩토링 하기
2. redux를 사용해서 상태관리에 타입을 추가하며 리팩토링하기(공식문서)
typescript에서 react-router-dom import하고 싶을 때 설치방법
(x) npm install --save react-router-dom //import에 오류가 생긴다.
(o) npm install --save-dev @types/react-router-dom
타입스크립트 처음으로 지금 lint + react +prettier로 짜고 aws에 s3와 ecs로 배포하려는데, 초심자에게도 도움되게 주석 너무 잘 달아주셔서 감사합니다!