리액트에서 타입스크립트 코드 작성 방법 / 타입스크립트 + 리액트 사용법

리액트 useState 사용법

리액트 userState로 상태 변수 정의

import { useState } from "react";

// useState 함수로 상태 변수 정의
const [상태변수명, set상태변수명] = useState(""); // string 타입으로 추론 (권장)
또는
const [상태변수명, set상태변수명] = useState<string>(); // string | undefined 유니온 타입으로 추론

제네릭 함수 useState는 전달받은 초기값 타입으로 변수 타입을 추론합니다.
초기값이 없으면 undefined 타입이 되어 값을 할당할 수 없으므로 제네릭을 지정해야 합니다.

상태 변수 수정

// 인풋 값 변경 이벤트 핸들러
// React에서 제공하는 이벤트 객체 타입 사용하여 타입 안전성 확보
const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
  set상태변수명(e.target.value);
};

return (
  // 인풋 정의
  <input
    value={상태변수명}
    onChange={onChangeInput}
  />
);

상태 변수 값을 인풋에 바인딩하고, 인풋 변경 시 이벤트 핸들러를 실행하는 코드입니다.

배열 상태 변수 정의 및 수정

import { useState, useRef } from "react";

// 배열에 담길 값 타입 정의
interface 인터페이스명 {
  id: number;
  content: string;
}

// 배열 상태 변수 타입 정의
const [상태변수명, set상태변수명] = useState<인터페이스명[]>([]);

// useRef 함수로 순번 변수 정의
const idRef = useRef(1);

// 배열 값 추가 이벤트 핸들러
const onClickAddBtn = () => {
  set상태변수명([
    ...상태변수명,
    {
      id : idRef.current++,
      content : "값"
    }
  ]);
}

return (
  <button onClick={onClickAddBtn}>배열에 값 추가</button>
)

값 추가 버튼 클릭 시, 배열에 객체를 추가하는 코드 예시입니다.

useRef는 값이 바뀌어도 컴포넌트를 리렌더링하지 않아 화면에 영향을 주지 않습니다.
초기값을 null로 설정하고 DOM을 참조하는 것도 가능합니다.

상태 변수 값 변경 훅 정의

import { useEffect } from "react";

useEffect(() => {
  console.log(상태변수명);
}, [상태변수명]);

상태 변수 값 변경 시마다 함수가 실행되는 훅을 정의할 수 있습니다.
useEffect 함수 2번째 매개변수인 의존성 배열에 상태 변수를 넣어주면 됩니다.


리액트 함수형 컴포넌트 분리 방법

리액트 함수형 컴포넌트를 분리하며 props, 타입스크립트 타입을 함께 사용하는 예시입니다.

루트 컴포넌트 (App.tsx)

import { useState, useRef, useEffect } from "react";
import "./App.css";
import 자식컴포넌트명 from "./components/자식컴포넌트명";

// 배열 타입 정의
interface Todo {
  id: number;
  content: string;
}

function App() {
  // 배열 생성 및 타입 명시
  const [todos, setTodos] = useState<Todo[]>([]);
  
  const idRef = useRef(1);
  
  // 자식컴포넌트에서 string 타입 파라미터를 받아 배열에 값 추가하는 함수
  const onClickAdd = (text: string) => {
    setTodos([
      ...todos,
      {
        id:idRef.current++,
        content: text,
      },
    ]);
  };

  // 자식컴포넌트에서 number 타입 파라미터를 받아 배열에서 삭제하는 함수
  const onClickDelete = (id: number) => {
    // 전달받은 id와 일치하는 객체를 제외한 배열로 저장
    setTodos(todos.filter((todo) => todo.id != id));
  }

  useEffect(() => {
    console.log(todos);
  }, [todos]);

  return (
    <div className="App">
      <인풋자식컴포넌트명 onClickAdd={onClickAdd} />
      <hr />
      <div>
        {/* 배열 출력 */}
        {todos.map((todo) => (
          <div key={todo.id}>{todo.content}</div>
          또는
          <목록자식컴포넌트명 key={todo.id} {...todo} onClickDelete={onClickDelete} />
          또는
          <목록자식컴포넌트명 key={todo.id} id={todo.id} content={todo.content} onClickDelete={onClickDelete} />
        )}
      </div>
    </div>
  );
}

export default App;

자식컴포넌트에 props를 내려주고, 배열을 화면에 출력하는 루트 컴포넌트 예시입니다.

useState → useReducer 변환

import { useReducer, useRef, useEffect } from "react";
import "./App.css";
import 자식컴포넌트명 from "./components/자식컴포넌트명";

// 배열 타입 정의
interface Todo {
  id: number;
  content: string;
}

// actions 객체 타입 정의
// 서로소 유니온 타입으로 정의
// 의도치 않은 Action 객체 전달 방지
type Action = {
  type: "CREATE";
  data: {
    id: number;
    content: string;
  }
} | {
  type: "DELETE";
  id: number;
}

// 상태 변화 처리 함수
// dispatch 함수에 전달한 매개변수는 action 객체로 받음
function reducer(state: Todo[], action: Action) {
  switch (action.type) {
    case "CREATE":
      return [...state, action.data];
    case "DELETE":
      return state.filter((it) => it.id !== action.id);
    default:
      return state;
  }
}

function App() {
  // 배열 생성 (상태 변화 처리 함수, 초기값 전달)
  const [todos, dispatch] = useReducer(reducer, []);
  
  const idRef = useRef(1);
  
  // 자식컴포넌트에서 string 타입 파라미터를 받아 배열에 값 추가하는 함수
  const onClickAdd = (text: string) => {
    // useState set 함수 → useReducer dispatch 함수 사용
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        content: text
      }
    });
  };

  // 자식컴포넌트에서 number 타입 파라미터를 받아 배열에서 삭제하는 함수
  const onClickDelete = (id: number) => {
    // useState set 함수 → useReducer dispatch 함수 사용
    dispatch({
      type: "DELETE",
      id: id
    });
  }

  useEffect(() => {
    console.log(todos);
  }, [todos]);

  return (
    <div className="App">
      <인풋자식컴포넌트명 onClickAdd={onClickAdd} />
      <hr />
      <div>
        {/* 배열 출력 */}
        {todos.map((todo) => (
          <목록자식컴포넌트명 key={todo.id} {...todo} onClickDelete={onClickDelete} />
          또는
          <목록자식컴포넌트명 key={todo.id} id={todo.id} content={todo.content} onClickDelete={onClickDelete} />
        )}
      </div>
    </div>
  );
}

export default App;

useState로 만든 배열을 useReducer로 만들도록 변경한 App.tsx 코드입니다.
useState는 간단한 상태, useReducer는 복잡한 상태를 관리할 때 사용하면 좋습니다.

인풋 자식 컴포넌트

import { useState } from "react";

// Props 타입 정의
interface Props {
  // 배열에 값 추가 함수 타입
  onClickAdd: (text: string) => void;
}

export default function 인풋자식컴포넌트명(props: Props) {
  const [text, setText] = useState("");

  // 인풋 변경 시 상태 변경 함수
  // React에서 제공하는 이벤트 객체 타입 사용하여 타입 안전성 확보
  const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  // 배열에 값 추가 버튼 클릭 시 함수
  const onClickButton = () => {
    // props로 받은 함수 호출 (매개변수로 text 상태 변수 전달)
    props.onClickAdd(text);

    // 인풋 값 초기화
    setText("");
  }

  return (
    <div>
      <input value={text} onChange={onChangeInput} />
      <button onClick={onClickButton}>배열에 값 추가</button>
    </div>
  );
}

src/components 폴더 하위에 자식 컴포넌트 tsx 파일을 만듭니다.
버튼 클릭 시마다 부모 컴포넌트에 있는 배열에 객체를 추가하고 있습니다.

목록 자식 컴포넌트

import { useState } from "react";

// Props 타입 정의
interface Props {
  id: number;
  content: string;
  onClickDelete: (id: number) => void;
}

export default function 목록자식컴포넌트명(props: Props) {
  const onClickButton = () => {
    // 부모에서 받은 삭제 함수 호출
    props.onClickDelete(props.id);
  };

  return (
    <div>
      {props.id} 번 : {props.content} <button onClick={onClickButton}>삭제</button>
    </div>
  )
}

부모컴포넌트에서 받은 배열 객체를 출력하고, 삭제 버튼으로 삭제하는 자식컴포넌트 예시입니다.


공통 타입 파일 분리

공통 타입 정의 파일 작성

export interface Todo {
  id: number;
  content: string;
}

src 폴더에 types 또는 interfaces 폴더를 만들고 .ts 파일을 생성합니다.
다양한 컴포넌트에서 사용하는 타입을 정의 후 export로 내보냅니다.

공통 타입 정의 파일 사용 예시

import { Todo } from "../types/ts파일명";

// Props 타입 정의 (타입 정의 상속)
interface Props extends Todo {
  추가키?: 타입
}

export default function 자식컴포넌트명(props: Props) {

  return (
    <div>
      {props.id} : {props.content}
    </div>
  )
}

위와 같이, 컴포넌트에서 타입 정의 파일을 import하여 사용할 수 있습니다.


리액트 Context API 사용법

루트 컴포넌트 (App.tsx)

import React, { useReducer, useRef, useEffect, useContext } from "react";
import "./App.css";
import 자식컴포넌트명 from "./components/자식컴포넌트명";

// 배열 타입 정의
interface Todo {
  id: number;
  content: string;
}

// actions 객체 타입 정의
// 서로소 유니온 타입으로 정의
// 의도치 않은 Action 객체 전달 방지
type Action = {
  type: "CREATE";
  data: {
    id: number;
    content: string;
  }
} | {
  type: "DELETE";
  id: number;
}

// 상태 변화 처리 함수
// dispatch 함수에 전달한 매개변수는 action 객체로 받음
function reducer(state: Todo[], action: Action) {
  switch (action.type) {
    case "CREATE":
      return [...state, action.data];
    case "DELETE":
      return state.filter((it) => it.id !== action.id);
    default:
      return state;
  }
}

// 상태 공급 Context 생성 및 타입 정의
export const TodoStateContext = React.createContext<Todo[] | null>(null);

// 상태 변경 Context 생성 및 타입 정의
export const TodoDispatchContext = React.createContext<{
  onClickAdd: (text: string) => void;
  onClickDelete: (id: number) => void;
} | null>(null);

// useContext 공급 커스텀 훅 생성
// 자식컴포넌트에서 context 내 함수 호출 시 옵셔널 체이닝 사용하지 않기 위해 작성
export function useTodoDispatchContext() {
  const context = useContext(TodoDispatchContext);

  // 타입 좁히기 (null이 아닌지 체크)
  if (!context) throw new Error("TodoDispatchContext가 null 입니다.");

  return context;
}

function App() {
  // 배열 생성 (상태 변화 처리 함수, 초기값 전달)
  const [todos, dispatch] = useReducer(reducer, []);
  
  const idRef = useRef(1);
  
  // 자식컴포넌트에서 string 타입 파라미터를 받아 배열에 값 추가하는 함수
  const onClickAdd = (text: string) => {
    // useState set 함수 → useReducer dispatch 함수 사용
    dispatch({
      type: "CREATE",
      data: {
        id: idRef.current++,
        content: text
      }
    });
  };

  // 자식컴포넌트에서 number 타입 파라미터를 받아 배열에서 삭제하는 함수
  const onClickDelete = (id: number) => {
    // useState set 함수 → useReducer dispatch 함수 사용
    dispatch({
      type: "DELETE",
      id: id
    });
  }

  useEffect(() => {
    console.log(todos);
  }, [todos]);

  return (
    <div className="App">
      <TodoStateContext.Provider value={todos}>
        <TodoDispatchContext.Provider value=>
          <인풋자식컴포넌트명 />
          <hr />
          <div>
            {/* 배열 출력 */}
            {todos.map((todo) => (
              <목록자식컴포넌트명 key={todo.id} {...todo} />
              또는
              <목록자식컴포넌트명 key={todo.id} id={todo.id} content={todo.content} />
            )}
          </div>
        </TodoDispatchContext.Provider>
      </TodoStateContext.Provider>
    </div>
  );
}

export default App;

부모컴포넌트에서 props로 데이터를 전달하지 않고, Context 생성 후 하위컴포넌트들에서 전역적으로 사용할 수 있도록 Provider로 값을 공급합니다.
커스텀 훅, Context 생성 코드를 src/context/TodoContext.tsx 등 별도 파일로 분리하면 더 관리하기 좋습니다.

인풋 자식 컴포넌트

import { useState } from "react";
import { useTodoDispatchContext } from "../App";

export default function 인풋자식컴포넌트명() {

  // 부모컴포넌트 커스텀 훅 호출하여 context 공급받기
  const context = useTodoDispatchContext();

  const [text, setText] = useState("");

  // 인풋 변경 시 상태 변경 함수
  // React에서 제공하는 이벤트 객체 타입 사용하여 타입 안전성 확보
  const onChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  // 배열에 값 추가 버튼 클릭 시 함수
  const onClickButton = () => {
    // context 내 함수 호출 (매개변수로 text 상태 변수 전달)
    context.onClickAdd(text);

    // 인풋 값 초기화
    setText("");
  }

  return (
    <div>
      <input value={text} onChange={onChangeInput} />
      <button onClick={onClickButton}>배열에 값 추가</button>
    </div>
  );
}

부모컴포넌트 커스텀 훅 또는 useContext 훅을 통해 Context 값을 받아서 사용합니다.

목록 자식 컴포넌트

import { useState } from "react";
import { useTodoDispatchContext } from "../App";

// Props 타입 정의
interface Props {
  id: number;
  content: string;
}

export default function 목록자식컴포넌트명(props: Props) {
  // 부모컴포넌트 커스텀 훅 호출하여 context 공급받기
  const context = useTodoDispatchContext();

  const onClickButton = () => {
    // context 내 삭제 함수 호출
    context.onClickDelete(props.id);
  };

  return (
    <div>
      {props.id} 번 : {props.content} <button onClick={onClickButton}>삭제</button>
    </div>
  )
}

하위 컴포넌트에서 props, context를 모두 사용할 수도 있습니다.