1. 프로젝트 준비하기
1) typeScript 컴파일러 설치 : 브라우저에서는 ts코드를 인식하지 못하므로 ts를 js로 변환하는 컴파일러를 설치해야한다.
$ npm install -g typescript
-g : golbal 즉 전역으로 설치한다.
2) 버전확인
$ tsc --version
버전 4.9.4로 설치되었다.
3) react+typeScript 프로젝트 생성
$ npx create-react-app todo-ts --template typescript
리액트 프로젝트 설치 프로젝트 명 --타입스크립트 기반의(옵션)
-> 타입스크립트를 제공하는 리액트 프로젝트 생성
**에러
npm ERR! 404 Not Found - GET https://registry.npmjs.org/creat-react-app - Not found
이렇게 에러가 났는데, node.js 설치 폴더와 관련된 것이다.
node.js를 c드라이브에 설치를 했다면, c드라이브 내에서 프로젝트 파일을 만들고 생성해줘야한다.
설치완료 후에는 Happy hacking! 이라고 뜬다.
4) 불필요한 파일 삭제

5) 아이콘 라이브러리 설치
: 리액트에서 사용할 수 있는 아이콘 라이브러리로 아이콘을 리액트 컴포넌트처럼 사용할 수 있다
해당 페이지에 들어가면 있는

이부분을 복사해서 라이브러리를 설치하면 된다.
https://react-icons.github.io/react-icons
React Icons
React Icons Include popular icons in your React projects easily with react-icons, which utilizes ES6 imports that allows you to include only the icons that your project is using. Installation (for standard modern project) npm install react-icons --save Usa
react-icons.github.io
6) 프로젝트 실행
프로젝트를 만들면 그 프로젝트 폴더 안에 동일한 이름의 폴더가 하나 생기고, 그 안에 프로젝트가 생성된다.
그래서 먼저 디랙토리 변경을 해주고,
$ cd Todo-ts
그다음 프로젝트를 실행한다.
$ npm start
**에러
[eslint] Plugin "react" was conflicted between "package.json » eslint-config-react-app
» C:\FEWorkspace\todo-ts\todo-ts\node_modules\eslint-config-react-app\base.js" and "BaseConfig
» C:\FEWorkspace\Todo-ts\todo-ts\node_modules\eslint-config-react-app\base.js".
나는 프로젝트 파일을 Todo-ts라고 설정을 했는데(첫글자 대문자),
디렉토리 변경 시 $ cd todo-ts 이렇게 소문자로 했더니 오류가 생겼다.
대문자로 변경하여 다시 실행했더니 정상작동 되었다.
2. 프로젝트 구조짜기
1) 필요한 기능 정의
기본 기능으로는
아이템 추가
아이템 삭제
전체 아이템 목록 출력
이렇게 3가지 이다.
향후계획으로는
아이템 체크박스
수정하기
다크모드
등.....^^
이 포스트에서는 기본기능까지만 다룬다.
2) 구조 : 기능에 따라 컴포넌트로 나누기

3) 필요한 파일 준비
- component 폴더 : InsertTodo.tsx, Todos.tsx, TodoItem.tsx, todo.ts
- style 폴더 : InsertTodo.css, Todos.css, TodoItem.css
여기까지해서 프로젝트 준비가 끝났고 이제 코딩을 시작한다..!
3. 타입을 지정하여 할일 띄우기
App.tsx에 Todos 컴포넌트를 불러오고, items라는 props로 배열을 보내준다.
import './App.css';
import InsertTodo from './components/InsertTodo';
function App() {
return (
<div className="App">
<Todos items={["청소하기", "공부하기"]} />
</div>
);
}
export default App;
Todos.tsx 컴포넌트의 타입과 prop인 items의 타입을 지정해준다.
import React from 'react';
const Todos: React.FC<{ items: string[] }> = (props) => {
// Todos 컴포넌트의 타입을 함수형 컴포넌트로 지정하고
// <>안에 items props의 타입을 string배열로 정한다.
// props인 items로 map함수를 돌린다.
return (
<div>
<ul>
{props.items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
export default Todos;
4. 할 일을 클래스 구조로 만들기
: 여기서는 화면에서 그리는 건 아니고 로직만 설정한다.
todo.ts
class Todo {
id: string;
text: string;
constructor(text: string) {
//id를 현재 일시로 겹치지 않게 지정하면 따로 id를 정하지 않아도 된다.
this.id = new Date().toLocaleString();
this.text = text;
}
}
export default Todo;
* 파일이름이 todo.ts인 이유?
컴포넌트(첫글자 대문자)와 구별하기 위해
* new Date().toLocaleString() 으로 현재 날짜와 시간을 받아오는데,
2022.12.18 오후 11:30:15
이런식으로 초 단위까지 나와서 중복되지 않으므로, 이를 id로 설정한다.
App.tsx Todo 클래스로 새로운 배열 todos 만들기
function App() {
const todos = [new Todo("영화보기"), new Todo("설거지")];
return (
<div className="App">
<InsertTodo />
<Todos items={todos}/>
{/* 기존 items의 타입이 string[]로 되어있어 타입오류가 난다 */}
</div>
);
}
Todos.tsx items의 타입이 Todo인 배열로 변경하고,
Todo클래스의 속성으로 id와 text가 있으므로 li태그의 매개변수 속성명을 변경해준다.
const Todos: React.FC<{ items: Todo[] }> = (props) => {
return (
<div>
<ul>
{props.items.map((item) => (
<li key={item.id}>{item.text}</li>
))}
</ul>
</div>
);
};
5. 컴포넌트 분리하기
Todos.tsx 의 이 부분을 컴포넌트로 분리하기
<li key={item.id}>{item.text}</li>
-> TodoItem 컴포넌트로
<TodoItem key={item.id} text={item.text} />
TodoItem.tsx 컴포넌트와 props의 타입설정, 그리고 삭제버튼은 아이콘을 사용한다.
import React from 'react';
import { FiPlusCircle } from "react-icons/fi";
const TodoItem: React.FC<{text: string}> = (props) => {
return (
<li>
<div>
{props.text}
</div>
<div>
<button><FiPlusCircle /></button>
</div>
</li>
);
};
export default TodoItem;
6. 입력 기능에서 submit 이벤트 추가하기
InsertTodo.tsx 버튼 클릭 시 실행되는 submitHandler 함수
import React from 'react';
import { FiPlusCircle } from "react-icons/fi";
import '../style/InsertTodo.css';
const InsertTodo = () => {
//e는 이벤트 객체인데 타입을 명시하지 않으면 오류가 난다 -> (e: React.FormEvent)로 작성
//(e: React.FormEvent) : e는 리액트가 제공하는 폼 이벤트이다.
const submitHandler = (e: React.FormEvent) => {
e.preventDefault();
}
return (
<div>
<form onSubmit={submitHandler}>
<input type="text" />
<button type="submit">
<FiPlusCircle />
</button>
</form>
</div>
);
};
export default InsertTodo;
7. 입력기능에서 input 값 변경 시 이벤트 추가
InsertTodo.tsx input 값 변경 시 실행되는 changeHandler 함수
const InsertTodo = () => {
const [text, setText] = useState("");
//폼이벤트 중 input에 발생한 이벤트만
const changeHandler = (e: React.FormEvent<HTMLInputElement>) => {
//typeScript에서는 e.target이 아니라 e.currentTarget로 사용
setText(e.currentTarget.value);
console.log(text);
}
return (
<div>
<form onSubmit={submitHandler}>
<input type="text" onChange={changeHandler}/>
<button type="submit">
<FiPlusCircle />
</button>
</form>
</div>
);
};
8. 변경된 text(input 값)를 데이터 전송하는 함수에 연결하기
InsertTodo.tsx 컴포넌트와 props의 타입설정
import React, { useState } from 'react';
import { FiPlusCircle } from "react-icons/fi";
import '../style/InsertTodo.css';
//props 타입 설정 : onAddTodo의 값으로 addTodoHandler를 받고 <>안에는 addTodoHandler의 타입을 적어준다.
//<{ onAddTodo: (매개변수: 타입) => 반환값타입 }>
//반환값이 void인 이유 : return이 없으므로
const InsertTodo: React.FC<{ onAddTodo: (item: string) => void }> = (props) => {
const [text, setText] = useState("");
const submitHandler = (e: React.FormEvent) => {
e.preventDefault();
//text에 앞뒤 공백을 제거했을때 공백이 아니라면, props인 onAddTodo에 text전달하고 input창 비워주기
//부모 컴포넌트의 onAddTodo 함수 호출
if(text.trim() !== "") {
props.onAddTodo(text);
setText("");
}
console.log("button에 대한 이벤트가 발생했습니다.");
}
//폼이벤트 중 input에 발생한 이벤트만
const changeHandler = (e: React.FormEvent<HTMLInputElement>) => {
//typeScript에서는 e.target이 아니라 e.currentTarget로 사용
setText(e.currentTarget.value);
console.log(text);
}
return (
<div>
<form onSubmit={submitHandler}>
<input type="text" onChange={changeHandler}/>
<button type="submit">
<FiPlusCircle />
</button>
</form>
</div>
);
};
export default InsertTodo;
App.tsx todo를 useState로 관리하게 하고, setTodos함수를 이용하여 todos의 값을 변경할 수 있다.
InsertTodo 컴포넌트의 props로 addTodoHandler라는 새로운 배열을 리턴하는 함수를 연결한다.
import { useState } from 'react';
import './App.css';
import InsertTodo from './components/InsertTodo';
import Todo from './components/todo';
import Todos from './components/Todos';
function App() {
// todos를 useState()로 관리하기 : useState는 generic 타입으로 Todo[]타입을 받겠다
const [todos, setTodos] = useState<Todo[]>([]);
//새로운 todo를 생성하여 기존todo+새로운todo배열 리턴하는 함수
const addTodoHandler = (item: string) => {
const newTodo = new Todo(item);
setTodos((prevTodo) => {
return [...prevTodo, newTodo];
})
console.log(`새로운 todo를 이벤트가 받는다 : ${newTodo}`);
console.log(`새로운 Todo-List : ${todos}`);
}
return (
<div className="App">
{/* 입력 컨포넌트의 props에 addTodoHandler함수 보내기 */}
<InsertTodo onAddTodo={addTodoHandler} />
<Todos items={todos}/>
</div>
);
}
export default App;
** 궁금한점
콘솔에 newTodo와 todos를 찍으면 입력한 todo객체와 todo객체 배열이 나올 것이라고 예상했는데,
[object Object] 라고 나온다.
왜 이렇게 나올까?

-> 문자열 리터럴인 백틱을 사용하여 newTodo와 todos를 출력했기 때문이다.
백틱을 빼고 변수만 출력하면 todo객체가 출력된다.
그런데 todos의 경우, 전체 todo가 아닌 마지막에 입력한 todo를 제외하고 출력이 되었다.

이것은 useState가 비동기 함수이기때문에 todo를 변경하는 함수 setTodos가 실행되기전에 콘솔이 먼저 실행이 된 것이다. 그렇다면 console.log(todos)를 밖으로 빼서 실행한다면?
const addTodoHandler = (item: string) => {
const newTodo = new Todo(item);
setTodos((prevTodo) => {
return [...prevTodo, newTodo];
})
console.log(newTodo);
}
console.log(todos);

마지막 입력한 "셋"까지 출력 된 것을 확인 할 수 있다.
7. 삭제기능
Todos.tsx Todos 컴포넌트의 props로 onRemoveTodo를 추가하고,
props 타입은 onRemoveTodo가 받는 함수(removeTodoHandler)의 타입으로 지정한다.
const Todos: React.FC<{ items: Todo[];
onRemoveTodo: (item: string) => void }>
= (props) => {
return (
<div>
<ul>
{props.items.map((item) => (
<TodoItem key={item.id} text={item.text} id={item.id}
onRemoveTodo={props.onRemoveTodo}/>
))}
</ul>
</div>
);
};
* TodoItem에 id와 onRemoveTodo를 props로 추가한다.
* TodoItem에서 key가 있는데 id를 따로 설정하는 이유?
key는 map함수를 사용하기위해 설정해놓은 키로 props가 될 수 없다.
TodoItem.tsx TodoItem의 props타입을 지정해주고, 버튼에 온클릭 이벤트를 설정해준다.
const TodoItem: React.FC<{text: string;
id: string;
onRemoveTodo: (id: string) => void}>
= (props) => {
return (
<li>
<div>
{props.text}
</div>
<div>
<button onClick={() => props.onRemoveTodo(props.id)}><FiPlusCircle /></button>
</div>
</li>
);
};
App.tsx removeTodoHandler 함수는 Todos컴포넌트의 onRemoveTodo props에 전달되는 함수이다.
removeTodoHandler는 Todos를 변경하는 setTodos를 사용하고,
filter함수를 이용하여 기존데이터에서 id가 일치하지 않는 것들만 뽑아서 리턴한다.
function App() {
// 생략
const removeTodoHandler = (id: string) => {
setTodos((prevTodos) => {
return prevTodos.filter((todo) => todo.id !== id);
})
}
return (
<div className="App">
{/* 입력 컨포넌트 연결하기 */}
<InsertTodo onAddTodo={addTodoHandler} />
<Todos items={todos} onRemoveTodo={removeTodoHandler}/>
</div>
);
}
todoItem.tsx에서 removeTodoHandler 함수를 사용하기 위해 todos.tsx를 거쳐 props로 계속 전달되었다.
App.tsx -> Todos.tsx -> TodoItem.tsx
이렇게 중간 컴포넌트가 해당데이터를 필요로 하지 않음에도 자식 컴포넌트에 전달하기 위해 props를 전달하는 과정을 Prop Drilling이라고 한다.
<결과>

이게 뭐고....? 싶겠지만
일단 기능만 돌아가게 만들었다..
완성(?)은 했는데 아직 잘 이해가 가지 않는 부분도 있어서 더 공부해야한다.
지금은 아이콘도 막 붙여 넣었고, 추가하고 인풋창도 안비워져있는데 차차 해야지😇
새싹DT 기업연계형 프론트엔드 실무 프로젝트 과정 9주차 블로그 포스팅
'React' 카테고리의 다른 글
| [React-Query] 1. 리액트쿼리 들어가기 전 Axios 알아보기 (0) | 2023.05.31 |
|---|---|
| [새싹 프론트앤드] react 메뉴 클릭 시 글자 색 변경 유지하기(NavLink) (0) | 2023.01.15 |
| [새싹 프론트엔드] Hooks정리(1) : useState, useEffect, useRef (0) | 2022.12.11 |
| [새싹 프론트앤드] 리액트 state, useState 함수, state 끌어올리기 (0) | 2022.12.04 |
| [새싹 프론트엔드] 리액트 이벤트 핸들링 (0) | 2022.12.04 |