Form을 가진 새로운 NewText.tsx
컴포넌트를 생성한다.
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}>
//...
todoTextInputRef.current?.value;
string | undefined
이 나온다.문자열
, 아직 연결되지 않았으면 null
이 입력되어 undefined
타입이 된다.이름은 있지만 나이는 옵셔널하게 가지는 변수를 지정해보자.
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이 아니라는 것을 백퍼 확신하는 경우에만 사용해야 한다. 즉, 확실히 연결 완료되었을 때만 사용해야 한다.string
만 나온다.이 기능은 아주 유용하다. 이 연산자들은 타입스크립트에서 중요한 연산자인데, 특히 레퍼런스를 다룰 때 매우 중요한 기능을 한다. 레퍼런스는 null일수도 있고 어떤 시점에서는 연결된 값이 없을 수도 있기 때문이다.
사용자 입력값을 키보드 입력마다 받지 않고 한번에 받기 위해 useState()
대신 useRef()
사용해보자.
바닐라 자바스크립트에서는 타입 표기가 없기 때문에 타입을 표기하지 않아도 아무런 문제가 없지만, 타입스크립트에서는 타입을 명시하지 않으면 아래와 같은 오류가 발생한다.
ref에는 useRef()로 생성한 레퍼런스를 넣어야 한다는 오류가 뜬다.
레퍼런스를 만드는 시점
에 타입스크립트가 이 레퍼런스가 input에 연결될거라는 사실을 알지 못하고 있다가 나중에 지정하고 나서야 알게 된다. 그래서 이런 오류가 발생한다.
const todoTextInputRef = useRef<HTMLInputElement>(null);
HTMLInputElement
처럼, 모든 돔 요소들은 미리 정의된 타입을 가진다.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;
const NewTodo: React.FC<{ onAddTodo: (text: string) => void }> = (props) => { ... }
직접 만든 props를 사용한다면 FC 타입이 제네릭 타입<>
이라는 특성을 이용해 props 객체를 구체적으로 정의해야한다.
onAddTodo
는 함수
이고, 반환하는 값은 없고
, 문자열 타입의 매개변수를 하나
받는다.
<{ onAddTodo: () => {} }>
함수 타입
인 props.onAddTodo
를 받는다.화살표 함수
() =>
로 하면된다.(text: string)
괄호
에 넣고, 타입을 지정하면 된다.<{ onAddTodo: (text: string) => void }>
화살표 다음에 작성
하면 된다.void
(무효) 타입을 넣으면 된다.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 (
//...
현재 App 컴포넌틔 외부에 todos 배열을 임의로 만들어 두었다.
const todos = [new TodoClass(["test1"]), new TodoClass(["test2"]), ];
useState()를 활용하여 todos 배열에 대해 상태를 관리하여, 폼에서 사용자 입력값을 저장하면 배열을 변경하여 App 컴포넌트 화면을 재렌더링하게 해보자.
const [todos, setTodos] = useState<TodoClass[]>([]);
setTodos에 마우스 커서를 대면 React.Dispatch
타입임을 확인할 수 있다.
이 타입은 상태 업데이트 함수가 가지는 타입으로 이 함수를 호출해 state를 변경할 수 있다. 즉, state업데이틀 요청하는 함수 타입이다.
todos 상태에 마우스 커서를 대면 never[]
타입이 추론 된다.
useState()의 초기값으로 그냥 빈배열[]
을 보내면 타입스크립트는 나중에 이 배열에 어떤 타입의 값이 들어올지 모르게된다.
그러면 타입스크립트는 todos의 타입이 never인 배열로 타입추론을 한다.
이 뜻은 이 배열이 언제나 비어있어야 한다는 의미이다. 그러면 어떤 값도 추가될 수 없게 되어버린다! 😨
따라서 처음에는 빈 배열이지만, 나중에 이 배열을 TodoClass
타입으로 채울 것이란 것을 확실히 해야 한다.
useState<TodoClass[]>([]);
useState()함수가 원래 제네릭 함수이기 때문에 제네릭 타입<>
을 사용하여 todos state에 정말로 저장하려고 하는 데이터의 타입이 🔥 TodoClass 타입을 가지는 배열, 즉 TodoClass[]
타입임을 표기하면 된다.
const addTodoHandler = (newTodoText: string) => {
const newTodo = new TodoClass(newTodoText);
setTodos((prevTodos) => {
return prevTodos.concat(newTodo);
});
};
함수를 onAddTodo 프로퍼티를 통해 <NewTodo />
컴포넌트로 전달하려면 <NewTodo />
컴포넌트에서 명시했던 props 타입과 동일한 형태로 함수를 만들어야 한다. (당연 ㅇㅇ!)
const addTodoHandler = (newTodoText: string) => { ... }
<NewTodo />
컴포넌트에서 표기한 대로 string
타입이다.const newTodo = new TodoClass(newTodoText);
setTodo((prevTodos) => { return prevTodos.concat(newTodo)})
concat()
메서드를 사용하여 새로운 배열을 반환하고, 그 반환값으로 상태를 업데이트한다.newTodo
가 포함된 새로운 배열로 state를 업데이트한다.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;