[TypeScript] React + Typescript

걸음걸음·2023년 3월 28일
0

TypeScript

목록 보기
8/8

React + TypeScript

CRA를 사용해서 타입스크립트 프로젝트를 생성할 때

npx create-react-app [프로젝트명] --template=typescript
// 혹은
npx create-react-app [프로젝트명] --template typescript

이미 생성된 프로젝트에 타입스크립트를 추가할 때

npm install typescript @types/node @types/react @types/react-dom @types/jest
  • .tsx : jsx 코드를 사용할 수 있는 확장자
  • react-app-env.d.ts : React와 타입스크립트를 연결하는 파일
    /// <reference types="react-scripts" />
  • package.json
    '@types'패키지 : 바닐라 자바스크립트 라이브러리와 타입스크립트 프로젝트 사이의 번역기 역할

React와 Type

const App: React.FC = () => {
  return <div className='App'></div>;
};

React. : 리액트에서 제공하는 타입
(node_modules의 @types 폴더에서 찾을 수 있음)
FC : Function Component(jsx를 리턴)

React.FC 는 사용하지 않는 추세가 되고 있음 ? (의견차이 있음)

제네릭을 지원하지 않음

컴파운드 컴포넌트 패턴의 타이핑을 혼란스럽게 함

defaultProps를 사용할 수 없게 됨

Children을 암시적으로 보유

React.FC 는 해당 컴포넌트가 props로 children를 가지고 있음을 암시적으로 전달
children을 다루고 있지 않은 컴포넌트에 children을 넘겨주어도 타입 에러가 발생하지 않는다

const NoChildren: React.FC = () => {
  return <div>Hello, world!</div>;
};
const App = () => {
  <NoChildren>Helllllllo!!!</NoChildren>;
};

해당 암묵적 선언은 18버전에서 제거됨
children을 사용하고 싶을 때에는 children: React.ReactNode로 따로 타입을 지정해줘야 함

  • React.FC를 사용하지 않을 경우 타입스크립트는 해당 타입을 JSX.Element 로 추정

React.FC vs JSX.Element

컴포넌트에 리턴값이 있을 경우 : JSX.Element
변수에 타입을 설정해야 하는 경우 : React.FC<type>

Props의 타입

타입스크립트를 리액트와 같이 사용할 때에는 이용하는 타입을 명확하게 지정해야 함
컴포넌트에 프롭을 사용한다면 타입스크립트로 이 프롭이 어떻게 생겼는지, 어떤 구조를 가지고 있는지 명시

interface TodoListProps {
  todos: { id: number; text: string }[];
}
// 프롭으로 받을 타입을 <> 안에 명시
const TodoList: React.FC<TodoListProps> = ({ todos }) => {
  return (
    ...
  );
};
  • Parameter 'props' implicitly has an 'any' type
    'props' 매개변수가 현재 any 타입이라는 경고 메세지
    (타입을 지정하지 않았을 때 발생, 만약 any 타입을 명시적으로 지정하면 해당 경고 메세지는 발생하지 않음)

React.FC 는 제네릭 타입
이미 내부에서 홀화살괄호를 사용해 정의된 상태
따라서 Todos:React.FC<{...}> 는 새로운 제네릭 타입을 만드는 것이 아니라, 내부적으로 사용되는 제네릭 타입에 구체적인 값을 추가하는 것
...에 필요한 형태의 props 정의
FC는 'key'같은 특별한 프로퍼티를 컴포넌트에 추가해 사용할 수 있게 해줌

Form 이벤트

const NewTodo = () => {
  const submitHandler = (event: React.FormEvent) => {
    event.preventDefault();
  };
  return (
    <form onSubmit={submitHandler}>
    ... 
    </form>
  );
};

onSubmit에 전달되는 evnet는 FormEvent임을 명시해야 함

ref 으로 사용자 입력 받기

input 안의 내용을 불러오는 방법

  • useState : 양방향 바인딩으로 관리
    양방향 바인딩 : JavaScript와 HTML 사이에 ViewModel이 존재해 둘 중 하나만 변경되어도 함께 변경되는 것 / 부모 컴포넌트에서 자식 컴포넌트로 props를 통해 데이터를 전달하고 자식 컴포넌트에서 부모 컴포넌트로 이벤트를 통해 데이터를 전달하는 구조
    리액트는 단방향 바인딩 라이브러리(?) - 강사님은 후자의 의미로 말씀하신 듯?
  • useRef : 제출될 때 유저가 입력한 내용 추출

useRef

useRef은 제네릭 타입으로 정의되어 있음
따라서 해당 useRef으로 생성할 레퍼런스가 어떤 타입일지 명확히 설정해야 함
useRef 레퍼런스가 다른 값에 할당되어있을 수도 있기 때문에 초기값으로 null을 지정해줘야 함
(지정하지 않으면 input에 ref를 연결할 때 오류 발생)

useRef이 연결된 부분이 input이기 때문에 HTMLInputElement 타입 전달

const NewTodo = () => {
  const todoTextInputRef = useRef<HTMLInputElement>(null);
  const submitHandler = (event: React.FormEvent) => {
    event.preventDefault();
    const enteredText = todoTextInputRef.current!.value;
  };
  return (
    ...
      <input type='text' id='text' ref={todoTextInputRef} />
    ...
  );
};

todoText.inputRef.current.value;에서 타입스크립트는 해당 코드가 실행될 때 current값이 있음을 확인하지 못하므로, 해당 값이 있음을 확신시켜주기 위해 ! 추가
( ! : 해당 값이 절대로 null 이 아님을 확신할 때 사용
? : 해당 값이 있는지 없는지 확신할 수 없을 때 사용.
일단 값에 접근해보고 접근이 가능하다면 입력된 값 사용, 접근이 불가능하다면 null 값 저장 )

상태 및 타입 작업

어플리케이션의 여러 위치에서 / 자주 사용하는 아이템인 경우 ().model.ts 처럼 새로운 파일을 만들어 모음

//todo.module.ts
export interface Todo {
  id: number;
  text: string;
}
//App.tsx
  const [todos, setTodos] = useState<Todo[]>([]);

useState를 단순히 빈배열로 초기화만 할 경우, 타입스크립트는 해당 상태가 항상 빈 배열일 것이라고만 추측
해당 상태가 어떻게 구성될 것인지를 따로 알려줘야 할 필요가 있음

useState는 상태가 비동기적으로 업데이트 되기 때문에 변경사항이 즉시 반영되지 않음
따라서 setTodos([...todos, { id: todos.length, text: text }]); 코드의 경우 해당 todos가 가장 최신 상태가 아닐 수 있기 때문에, 상태를 업데이트 할 때 최신 상태를 사용할 것을 보장하기 위해 함수 사용
setTodos((prev) => [...prev, { id: todos.length, text: text }]);

class 는 생성자 역할과 타입 역할 모두 사용 가능

// todo.ts
class Todo {
  id: string;
  text: string;
  constructor(todoText: string) {
    this.text = todoText;
    this.id = new Date().toLocaleDateString();
  }
}
//App.tsx
  const todos = [new Todo('할 일'), new Todo('할 일2')];
//Todos.tsx
const Todos: React.FC<{ items: Todo[] }> = (props) => {
  ...
};

function props

props으로 function을 받을 때

const NewTodo: React.FC<{ onAddTodo: (text: string) => void }> = (props) => {
  const submitHandler = (event: React.FormEvent) => {
    ...
    props.onAddTodo(enteredText);
  };
};

다른 타입의 props를 받을 때와 마찬가지로 타입 지정 필요
onAddTodo 함수는 반환 값이 없기 때문에 void 지정

const TodoItem: React.FC<{
  text: string;
  onRemoveTodo: (event: React.MouseEvent) => void;
}> = (props) => {
  return (
    <li className={classes.item} onClick={props.onRemoveTodo}>
      {props.text}
    </li>
  );
};

이 경우 인수를 사용하지 않기 때문에, 인수의 타입(event: React.MouseEvent)을 정의하는건 선택사항이 됨

const Todos: React.FC<{ items: Todo[]; onRemoveTodo: (id: string) => void }> = (
  props
) => {
  return (
    ...
        <TodoItem
          ...
          onRemoveTodo={props.onRemoveTodo.bind(null, item.id)}
          ...
};

bind를 사용해서 실행할 함수를 미리 설정 가능
onRemoveTodo가 매개변수로 받을 값을 두번째로 전달
(해당 id값을 전달하기 위해 bind 사용, this 키워드는 현재 필요 없기 때문에 null로 두고 두 번째 매개변수가 해당 함수가 받을 첫번째 매개변수가 됨)

더 많은 props 및 상태 작업

삭제 기능을 추가하기 위해 id를 인자로 받는 함수를 TodoList로 전달, button의 onClick 이벤트에 해당 함수를 연결한다

interface TodoListProps {
  todos: { id: number; text: string }[];
  onDeleteTodo: (id: number) => void;
  // 프롭으로 받는 아이템에 함수 추가
}
const TodoList: React.FC<TodoListProps> = ({ todos, onDeleteTodo }) => {
  return (
    <ul>
      {todos.map((ele) => (
        ...
          <button onClick={onDeleteTodo.bind(null, ele.id)}>Del</button>
          // 해당 id값을 전달하기 위해 bind 사용, this 키워드는 현재 필요 없기 때문에 null로 두고 두 번째 매개변수가 해당 함수가 받을 첫번째 매개변수가 됨
        ...
      ))}
    </ul>
  );
};

Context API 사용

Context API : 데이터를 전역적으로 사용할 수 있게 해주는 리액트의 내장 기능
Props Drilling 문제를 피하고자 할 때 사용 가능
상태관리 도구가 아님(상태 관리는 직접 해야 함, 전역적으로 상태를 공유해주는 기능만 수행)
useContext를 사용한 모든 컴포넌트에 리렌더링 문제가 발생

import React, { useState } from 'react';
import Todo from '../models/todo';

type TodosContextObj = {
  items: Todo[];
  addTodo: (text: string) => void;
  removeTodo: (id: string) => void;
};

export const TodosContext = React.createContext<TodosContextObj>({
  items: [],
  addTodo: () => {},
  removeTodo: (id: string) => {},
});

// 18v 부터 React.FC가 children을 암묵적으로 허용하던 부분이 사라졌기 때문에, ReactNode로 새롭게 children 타입을 정해줘야 함
const TodosContextProvider: React.FC<{ children: React.ReactNode }> = (
  props
) => {
  // App.tsx에서 사용하던 상태관리 코드 가져오기
  const [todos, setTodos] = useState<Todo[]>([]);
  const addTodoHandler = (text: string) => {
    const newTodo = new Todo(text);
    setTodos((prev) => [...prev, newTodo]);
  };
  const removeTodoHandler = (todoId: string) => {
    setTodos((prev) => {
      return prev.filter((todo) => todo.id !== todoId);
    });
  };

  // Provider로 전달할 value값 지정
  const contextValue: TodosContextObj = {
    items: todos,
    addTodo: addTodoHandler,
    removeTodo: removeTodoHandler,
  };

  return (
    <TodosContext.Provider value={contextValue}>
      {props.children}
    </TodosContext.Provider>
  );
};

export default TodosContextProvider;
//App.tsx
function App() {
  return (
    // useContext를 사용하기 위해서는 해당 컨텍스트를 사용하는 컴포넌트들을 Provider로 감싸야 함
    <TodosContextProvider>
      <NewTodo />
      <Todos />
    </TodosContextProvider>
  );
}

// Todos.tsx
const Todos: React.FC = () => {
  // props 대신 Context로 상태값을 가지고 와서 사용할 수 있음(코드를 더 깔끔하게 사용 가능)
  const todosCtx = useContext(TodosContext);
  return (
    <ul className={classes.todos}>
      {todosCtx.items.map((item) => (
        <TodoItem
          key={item.id}
          text={item.text}
          onRemoveTodo={todosCtx.removeTodo.bind(null, item.id)}
        />
      ))}
    </ul>
  );
};

export default Todos;

react-route-dom 사용

npm i @types/react-router-dom 설치 필요

profile
꾸준히 나아가는 개발자입니다.

0개의 댓글