[Type Script] Todo 앱 연습: Todo 추가하는 Form 만들기

summereuna🐥·2023년 8월 19일
0

TypeScript

목록 보기
3/13

Form을 가진 새로운 NewText.tsx 컴포넌트를 생성한다.

✅ Form

📝 form 이벤트에 대한 event의 타입


리액트의 onSubmit 프로퍼티로 submit 이벤트 등록 시, event의 타입은 React.FormEvent 이다.

  • const submitHandler = (event: React.FormEvent) => { //..

  • React.FormEvent
    이 이벤트 객체타입은 리액트 패키지가 제공하는 타입으로, 폼 제출 이벤트를 수신하면 자동적으로 받게 된다.

  • (참고) React.MouseEvent는 onClick 이벤트 리스너를 등록하면 받게되는 타입이다.

  • submitHandler()의 이벤트의 타입으로 React.FormEvent가 아닌 React.MouseEvent를 넣어보면, 리액트가 예상한 함수 타입과 다른 함수 타입을 지정한 경우 아래와 같이 오류 메시지가 뜬다.

  const submitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = todoTextInputRef.current!.value;
    //레퍼런스에는 항상 current 프로퍼티가 있고 이 안에 실제 값이 들어 있다.

    //가져온 값 검증: 입력된 값 없으면 리턴
    if (enteredText.trim().length === 0) {
      //throw an error
      return;
    }
    console.log(enteredText);
    //enteredText todos 목록에 추가하기
  }
  
    ///...
  
  return (
    <form onSubmit={submitHandler}>
      //...

📝 참고: JS의 ?, ! 연산자


? 연산자

  • todoTextInputRef.current?.value;
    레퍼런스를 사용하려는 시점에, 혹시 아직 값이 설정되지 않았다면 null이 enteredText에 저장되어 오류가 뜨지 않는다.
  • 그리고 enteredText에 마우스 커서를 가져다 대면 추론 타입string | undefined이 나온다.
    연결되면 문자열, 아직 연결되지 않았으면 null이 입력되어 undefined 타입이 된다.

선택적 변수(optional parameter) 지정하는 방법 예시

이름은 있지만 나이는 옵셔널하게 가지는 변수를 지정해보자.

const singer: {
  name: string,
  age?: number //age 있어도 되고 없어도 되게 설정
  //그러면 age: number | undefined
} = {
  name: "BTS"
}

//age가 있고 && age가 10보다 작을 경우에만 실행
if(singer.age && singer.age < 10) {
  //singer.age가 undefined이 아닐 때만 실행하게 && 코드를 작성하면 오류가 안뜸
}

! 연산자

  • todoTextInputRef.current!.value;
    만약 이 시점에서 레퍼런스와 요소가 확실히 연결되었다는 것을 알고 있다면 물음표 대신 느낌표를 사용할 수 있다.
  • 이 특수 기호는 타입스크립트에게 이 값이 null이 될 수 있다는 건 알지만 이 시점에는 절대 null이 아님을 알려준다.
    따라서 ! 연산자는 null이 아니라는 것을 백퍼 확신하는 경우에만 사용해야 한다. 즉, 확실히 연결 완료되었을 때만 사용해야 한다.
  • 그러면 추론 타입string만 나온다.

요약

이 기능은 아주 유용하다. 이 연산자들은 타입스크립트에서 중요한 연산자인데, 특히 레퍼런스를 다룰 때 매우 중요한 기능을 한다. 레퍼런스는 null일수도 있고 어떤 시점에서는 연결된 값이 없을 수도 있기 때문이다.

  • null일 수도 있는 값을 다룰 때는 물음표를 사용하여 먼저 해당 값에 접근해보고, null 일 경우 상수 또는 값을 저장할 곳에 null을 저장한다.
  • 느낌표를 사용하면 이 위치에서는 해당 값이 절대 null이 될 수 없으니 바로 객체의 프로퍼티로 들어가 null이 아닌 실제값을 가져온다.

✅ useRef()로 사용자 입력 받기


사용자 입력값을 키보드 입력마다 받지 않고 한번에 받기 위해 useState() 대신 useRef() 사용해보자.

📝 useRef로 생성한 레퍼런스의 타입


useRef()를 사용할 때는 레퍼런스를 만드는 시점에서 레퍼런스의 타입을 명확하게 설정해야 한다.

  • 바닐라 자바스크립트에서는 타입 표기가 없기 때문에 타입을 표기하지 않아도 아무런 문제가 없지만, 타입스크립트에서는 타입을 명시하지 않으면 아래와 같은 오류가 발생한다.

    ref에는 useRef()로 생성한 레퍼런스를 넣어야 한다는 오류가 뜬다.

  • 레퍼런스를 만드는 시점에 타입스크립트가 이 레퍼런스가 input에 연결될거라는 사실을 알지 못하고 있다가 나중에 지정하고 나서야 알게 된다. 그래서 이런 오류가 발생한다.

    • 즉, input 요소의 ref에 레퍼런스를 주긴했지만, 명확하게 이 HTML요소에 맞는! 레퍼런스가 아닌거다. button 요소의 ref에 가져다 넣을 수도 있는 노릇이니 말이다.

useRef()의 타입은 제네릭 타입<>으로 명시한다.

  • 따라서 이 레퍼런스에 저장될 데이터가 어떤 타입인지 명확히 명시해야 한다. 이를 위해 제네릭 타입을 사용한다. useRef 자체도 제네릭 타입으로 정의되어 있다.

특히 레퍼런스와 연결할 HTML요소를 구체적으로 설정해야 한다.

  • const todoTextInputRef = useRef<HTMLInputElement>(null);
  • HTMLInputElement 처럼, 모든 돔 요소들은 미리 정의된 타입을 가진다.

레퍼런스 생성시 useRef()에 디폴트 값을 항상 넣어줘야 오류가 안뜬다.

  • 인풋의 시작 값은 없으므로 null을 넣어주면 된다.
  • ""는 안된당..^^
import { useRef } from "react";

const NewTodo = () => {
  //사용자 입력값 키보드 입력마다 받지않고 한번에 받기 위해 useState 대신 useRef 사용
  //레퍼런스와 연결할 HTML요소를 구체적으로 설정해야 한다.
  //모든 돔 요소들은 미리 정의된 타입을 가진다.
  //그리고 디폴트 값을 항상 넣어줘야 한다. 시작값에 넣을게 없으면 null
  const todoTextInputRef = useRef<HTMLInputElement>(null);
  
  //...
  
  return (
    <form onSubmit={submitHandler}>
      <label htmlFor="text">Todo 입력</label>
      <input
        id="text"
        name="text"
        type="text"
        ref={todoTextInputRef}
        onChange={textChangeHandler}
        value={newText}
      ></input>
      <button type="submit">추가</button>
    </form>
  );
};

export default NewTodo;

✅ Todo 추가하기: 폼에서 받은 데이터 상위 컴포넌트로 올려보내기

📝 하위 컴포넌트에서 함수인 props에 대한 타입 명시하기


1. NewTodo 컴포넌트 타입 명시하기

NewTodo 컴포넌트에 함수형 컴포넌트 명시 위해 React.FC 타입 추가해야 한다.

  • const NewTodo: React.FC<{ onAddTodo: (text: string) => void }> = (props) => { ... }

2. NewTodo 컴포넌트가 받는 props의 타입 명시하기

직접 만든 props를 사용한다면 FC 타입이 제네릭 타입<>이라는 특성을 이용해 props 객체를 구체적으로 정의해야한다.

onAddTodo함수이고, 반환하는 값은 없고, 문자열 타입의 매개변수를 하나 받는다.

함수타입 표기

  • <{ onAddTodo: () => {} }>
    NewTodo 컴포넌트의 매개변수로 함수 타입props.onAddTodo를 받는다.
  • 함수 타입 표기화살표 함수 () =>로 하면된다.

함수의 매개변수에 대한 타입 표기

  • (text: string)
    함수 호출시 인수인 enteredText를 전달하고 있기 때문에, 함수의 매개변수에 대한 타입을 명시해야 한다.
  • 함수의 매개변수괄호에 넣고, 타입을 지정하면 된다.
  • 인수이름은 원하는대로 하면 된다.

함수의 반환 타입(리턴 타입)에 대한 표기

  • <{ onAddTodo: (text: string) => void }>
  • 함수의 반환 타입(리턴 타입)화살표 다음에 작성하면 된다.
  • 그런데 onAddTodo는 값을 반환할 필요가 없다. 함수를 호출하고 반환값을 받아 처리할 일이 없기 때문이다.
    반환값을 상수에 저장하거나 출력하지 않을 거기 때문에 반환값이 필요 없으므로, 반환값이 없다는 타입void(무효) 타입을 넣으면 된다.

📍 /src/components/NewTodo.tsx

import { useRef } from "react";

//NewTodo 컴포넌트가 React.FC 타입임을 표기
//이 컴포넌트가 받는 props의 onAddTodo 프로퍼티에 대해 <> 사용하여 구체적인 타입 표기

//onAddTodo는 함수이고, 반환하는 값은 없고, 문자열 타입의 매개변수를 하나 받는다.
const NewTodo: React.FC<{ onAddTodo: (text: string) => void }> = (props) => {
  const todoTextInputRef = useRef<HTMLInputElement>(null);

  const submitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = todoTextInputRef.current!.value;

    //가져온 값 검증
    if (enteredText.trim().length === 0) {
      //throw an error
      return;
    }
    //enteredText todos 목록에 추가하기
    //App 컴포넌트에 있는 onAddTodo 함수 props으로 받아 호출하여 데이터 올려보내기
    props.onAddTodo(enteredText);
  };

  return (
    //...

📝 상위 컴포넌트에서 받은 데이터 처리하기


1. useState()로 todos 배열 상태관리하기

현재 App 컴포넌틔 외부에 todos 배열을 임의로 만들어 두었다.

const todos = [new TodoClass(["test1"]), new TodoClass(["test2"]), ];

useState()를 활용하여 todos 배열에 대해 상태를 관리하여, 폼에서 사용자 입력값을 저장하면 배열을 변경하여 App 컴포넌트 화면을 재렌더링하게 해보자.

useState()의 초기값이 빈 배열일 경우 타입 표기 방법

const [todos, setTodos] = useState<TodoClass[]>([]);
  • setTodos에 마우스 커서를 대면 React.Dispatch 타입임을 확인할 수 있다.
    이 타입은 상태 업데이트 함수가 가지는 타입으로 이 함수를 호출해 state를 변경할 수 있다. 즉, state업데이틀 요청하는 함수 타입이다.

  • todos 상태에 마우스 커서를 대면 never[] 타입이 추론 된다.

    useState()의 초기값으로 그냥 빈배열[]을 보내면 타입스크립트는 나중에 이 배열에 어떤 타입의 값이 들어올지 모르게된다.
    그러면 타입스크립트는 todos의 타입이 never인 배열로 타입추론을 한다.
    이 뜻은 이 배열이 언제나 비어있어야 한다는 의미이다. 그러면 어떤 값도 추가될 수 없게 되어버린다! 😨

  • 따라서 처음에는 빈 배열이지만, 나중에 이 배열을 TodoClass 타입으로 채울 것이란 것을 확실히 해야 한다.

  • useState<TodoClass[]>([]);
    useState()함수가 원래 제네릭 함수이기 때문에 제네릭 타입<>을 사용하여 todos state에 정말로 저장하려고 하는 데이터의 타입이 🔥 TodoClass 타입을 가지는 배열, 즉 TodoClass[] 타입임을 표기하면 된다.


2. 사용자 입력 값 받아 상태 업데이트하기

const addTodoHandler = (newTodoText: string) => {
  const newTodo = new TodoClass(newTodoText);
  
  setTodos((prevTodos) => {
    return prevTodos.concat(newTodo);
  });
};

하위 컴포넌트로 전달되는 함수 addTodoHandler를 생성한다.

함수를 onAddTodo 프로퍼티를 통해 <NewTodo /> 컴포넌트로 전달하려면 <NewTodo /> 컴포넌트에서 명시했던 props 타입과 동일한 형태로 함수를 만들어야 한다. (당연 ㅇㅇ!)

  • const addTodoHandler = (newTodoText: string) => { ... }
  • 반환값이 없는 매개변수 하나 받는 함수를 만들면 된다.
  • 매개변수의 타입은 <NewTodo /> 컴포넌트에서 표기한 대로 string 타입이다.

new TodoClass 생성자를 호출하여 인수로 newTodoText 데이터를 전달한다.

  • const newTodo = new TodoClass(newTodoText);
  • state에 추가될 항목의 형태는 TodoClass 형태여야 하므로 인수로 받은 데이터를 new TodoClass 생성자를 호출하여 인수로 전달한다.

기존 배열에 concat()메서드 사용하여 상태를 업데이트 한다.

  • state를 업데이트하려면 바로 값을 넣지 말고, 기존 값을 사용한 함수 형태를 사용하여 state를 업데이트 해야한다.
  • setTodo((prevTodos) => { return prevTodos.concat(newTodo)})
    기존 배열에 concat() 메서드를 사용하여 새로운 배열반환하고, 그 반환값으로 상태를 업데이트한다.
  • 이렇게하면 리액트는 newTodo가 포함된 새로운 배열로 state를 업데이트한다.

📍 /src/App.tsx

import { useState } from "react";
import "./App.css";
import NewTodo from "./components/NewTodo";
import Todos from "./components/Todos";
import TodoClass from "./models/todo";

function App() {
  //처음엔 빈배열로 시작하지만, todos는 TodoClass를 가지는 배열이 타입임을 표기
  const [todos, setTodos] = useState<TodoClass[]>([]);
  
  const addTodoHandler = (newTodoText: string) => {
    //state에 추가될 항목의 형태는 TodoClass 형태여야 하므로 인수로 받은 데이터를 new TodoClass 생성자를 호출하여 인수로 전달한다.
    const newTodo = new TodoClass(newTodoText);
    //state를 업데이트하려면 바로 값을 넣지 말고 함수 형태를 사용하여 state를 업데이트 해야한다.
    setTodos((prevTodos) => {
      //기존 배열에 concat()메서드를 적용하여 새로운 배열을 반환
      return prevTodos.concat(newTodo);
    });
  };
  
  return (
    <div>
      <NewTodo onAddTodo={addTodoHandler} />
      <Todos items={todos} />
    </div>
  );
}

export default App;
  • 잘 작동한다. 🤓
profile
Always have hope🍀 & constant passion🔥

0개의 댓글