React + TS - form & input 처리

thousand_yj·2023년 7월 5일
0

Typescript & React

목록 보기
7/12

useRef

input에 입력된 값을 받아올 때 키가 눌릴 때마다 그 값을 받아오는 방법은 onChange메서드에서 useState를 사용하여 값을 설정해오는 것이다. 다만 투두리스트 시나리오에서는 값이 바뀔 때마다 그 값으로 state를 업데이트하는 것은 불필요하다. 즉, submit할때만 값을 받아오면 되는데 그 방법은 useRef hook 을 사용하는 것이다.

import { useRef } from "react";

function NewToDo() {
  const todoTextInputRef = useRef(); // 함수 안에는 초기값을 넘겨줄 수 있다.
  const sumbitHandler = (event: React.FormEvent) => {
    event.preventDefault();
  };
  return (
    <form onSubmit={sumbitHandler}>
      <input
        type="text"
        name="todoInput"
        id="todo-input"
        placeholder="할일을 입력하세요"
        ref={todoTextInputRef}
      />
      <button type="submit">✔️</button>
    </form>
  );
}

export default NewToDo;

ref prop

모든 HTML element에 기본적으로 제공되는 속성이다. 개발자가 원하는 reference와 연결할 수 있는 속성이다.
보통 input 요소와 많이 사용하지만, 모든 HTML 요소에서 사용 가능하다.

useRef() 함수가 리턴하는 데이터

current prop (ref가 연결된 DOM node 데이터)을 갖고 있는 object를 리턴한다. 모든 js 요소는 기본적으로 value를 갖고 있으며, 지금 필요한 데이터는 value 안에 들어가 있다. 따라서 안에서 값을 가져오려면 userInput.current.value를 사용하면 된다.

만약 input 창의 데이터를 reset하는 로직을 사용해야한다면, 권장하지 않는 방법이지만 ref를 사용하여 DOM을 수정하면 된다. 단순히 input의 값을 초기화하는 정도는 사용해도 되지만, ref를 사용하여 DOM을 manipulate하는 것은 권장되지 않는다.

todoTextInputRef.current.value = "";

다만 위와 같이 코드를 작성하면 에러가 난다. useRef는 모든 HTML element와 연결될 수 있어 정확히 todoTextInputRef이 어떤 값인지 ts가 추측할 수 없기 때문이다. MDN input 문서 내 다음 부분을 참고하면 어떤 자료형을 전달해야 되는지 체크할 수 있다.

mdn input

따라서 useRef 함수의 리턴값으로 <HTMLInputElement>를 전달해주면 된다.

여전히 에러는 해결되지 않으며 에러 코드는 다음과 같다.
Type 'HTMLInputElement | undefined' is not assignable to type 'HTMLInputElement | null'.

useRef에 기본 값을 전달해주지 않아 발생한 에러로, 명시적으로 초기값이 없다는 것을 의미하도록 null을 전달해주면 된다.

const todoTextInputRef = useRef<HTMLInputElement>(null);

submit 이벤트 처리

const sumbitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = todoTextInputRef.current.value; // 에러발생
  };

위 코드는 에러가 발생한다. 그 이유는 todoTextInputRef 요소를 찾은 순간에 current값이 존재하는지 존재하지 않는지 ts가 보장할 수 없기 때문이다. 현재 개발자는 이 코드가 submit이 될 때 호출될지 알기 때문에 ref 요소가 null이 아님을 알고 있다. 따라서 not null임을 보장하도록 !current뒤에 붙여주면 된다. (반대로, 값이 있으면 가져오고 없으면 가져오지 않도록 하려면 ?를 붙여주면 된다.)

import { useRef } from "react";

function NewToDo() {
  const todoTextInputRef = useRef<HTMLInputElement>(null);
  const sumbitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = todoTextInputRef.current!.value;

    if (enteredText.trim().length === 0) {
      // Throw an error
      return;
    }
  };
  return (
    <form onSubmit={sumbitHandler}>
      <input
        type="text"
        name="todoInput"
        id="todo-input"
        placeholder="할일을 입력하세요"
        ref={todoTextInputRef}
      />
      <button type="submit">✔️</button>
    </form>
  );
}

export default NewToDo;
if (enteredText.trim().length === 0) {
      // Throw an error
      return;
    }

이 부분은 유저가 입력한 값 없이 제출했을 때를 처리하는 코드이다. 일단은 비워두고 return만 하도록 한다.

이제 제출이 완료된 경우를 처리해주면 된다. 다만 제출된 ToDo와 관련된 부분은 다른 컴포넌트에서 렌더링할 때 사용해야 하므로 여기에서 단순히 처리해주면 안된다.
따라서 부모 컴포넌트에서 함수를 전달받아 호출만 해주도록 바꾸자.

최종적인 NewToDo 컴포넌트 코드는 다음과 같다.

import { useRef } from "react";
type NewToDoProps = {
  onAddToDo: (text: string) => void;
};
function NewToDo(props: NewToDoProps) {
  const todoTextInputRef = useRef<HTMLInputElement>(null);
  const sumbitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = todoTextInputRef.current!.value;

    if (enteredText.trim().length === 0) {
      // Throw an error
      return;
    }
    props.onAddToDo(enteredText);
  };
  return (
    <form onSubmit={sumbitHandler}>
      <input
        type="text"
        name="todoInput"
        id="todo-input"
        placeholder="할일을 입력하세요"
        ref={todoTextInputRef}
      />
      <button type="submit">✔️</button>
    </form>
  );
}

export default NewToDo;

이제 부모 컴포넌트(ToDoPage)로 넘어가보자.
현재 프로젝트 구조는 다음과 같다.
프로젝트 구조도

ToDoPage 컴포넌트에서 Todo 클래스를 사용하여 새로운 Todo를 생성한 뒤 해당 state를 ToDos로 전달해주면 된다.

import {useState} from 'react';

import NewToDo from "../components/ToDo/NewToDo";
import ToDos from "../components/ToDo/ToDos";
import Todo from "../models/todo";

function ToDoPage() {
  const [todos, setToDos] = useState([]); // todos는 never[]로 추론

  const addToDoHandler = (todoText:string) => {
    const newTodo = new Todo(todoText);
    setToDos((prev) => prev.concat(newTodo)); // 컴파일 에러
  }
  return (
    <>
      <ToDos items={todos} />
      <NewToDo onAddToDo={addToDoHandler} />
    </>
  );
}

export default ToDoPage;

위 코드는 에러가 난다. todos 배열의 자료형을 ts가 추론할 수 없어 초기값에 기반하여 아무 값도 추가될 수 없도록 never[]로 설정했기 때문이다. 따라서 직접 자료형을 다음과 같이 추가해줘야 한다.

const [todos, setToDos] = useState<Todo[]>([]);
profile
함께 일하고 싶은 개발자가 되기 위해 노력합니다. 코딩테스트 관련 공부 및 이야기는 티스토리에도 업로드되어 있습니다.

0개의 댓글