[TypeScript] React+Typescript로 TODO앱 만들기

HIHI JIN·2023년 3월 25일
1

typescript

목록 보기
9/10
post-thumbnail

타입스크립트 핸드북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;
}

React와 Typescript는 어떻게 함께 작동하는가

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 타입을 사용한다.

Props로 작업하기, props 타입

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;

'ref'로 사용자 입력받기

할일 추가하기
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;

Cross-component 커뮤니케이션

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;
}

더 많은 props 및 상태 작업

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;
  }

localstorage 기능 추가

//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() 호출
  ...
};

다른 react기능의 타입(redux, route)

이번 프로젝트로 시도 해봐야 할 것
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

CSS완료 후

JsonServer 연결

AWS 배포하기

배포링크

profile
신입 프론트엔드 웹 개발자입니다.

1개의 댓글

comment-user-thumbnail
2024년 11월 11일

타입스크립트 처음으로 지금 lint + react +prettier로 짜고 aws에 s3와 ecs로 배포하려는데, 초심자에게도 도움되게 주석 너무 잘 달아주셔서 감사합니다!

답글 달기