타입스크립트 리액트

익숙해질것 같으면서도 그렇지 않은 타입스크립트 부수기
타입스크립트 + 리액트 적용 정리
프로젝트 생성

npx create-react-app app-name --typescript

타입스크립트를 사용하는 리액트 컴포넌트는 *.tsx 확장자를 사용한다.
App.tsx

import React from 'react'
import logo from './logo.svg'
import './App.css'

const App: React.FC = () => {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  )
}

export default App

컴포넌트 만들어보기

import React from 'react'

type GreetingsProps = {
  name: string
}

const Greetings: React.FC<GreetingsProps> = ({ name }) => (
  <div>Hello, {name}</div>
)

export default Greeting

React.FC 장단점

React.FC 에서는 props의 타입을 generics로 넣어서 사용한다.

  • props에 children이 기본적으로 들어가있다.
  • 컴포넌트의 defaultProps, propTypes, contextTypes 를 설정할 때 자동완성이 된다.

단점으로는 children이 들어가있다보니 컴포넌트의 props의 타입이 명백하지 않다.
또한 defaultProps가 제대로 작동하지 않는다. 보통 React.FC의 사용은 권장되지 않는다.

생략 할 수 있는 props 설정하기

...
type GreetingsProps = {
  name: string;
  mark: string;
  optional?: string;
  onClick: (name: string) => void; // void는 아무것도 리턴하지 않는다.
}

function Greetings({name, mark, optional, onClick}: GreetingsProps) {
  const handleClick = () => onClick(name)
  return (
    <div>
      하이, {name} {mark}
      {optional && <p>{optional}</p>}
      <div>
        <button onClick={handleClick}>나를 눌러라</button>
      </div>
    </div>
  )
}

Greetings.defaultProps = {
  mark: '!'
};
...

부모 컴포넌트

...
const App: React.FC = () => {
  const onClick = (name: string) => {
    console.log(`${name} says hello`);
  };
  return <Greetings name="Hello" onClick={onClick} />;
}
...

컴포넌트를 렌더링 할 때 필요한 props를 빼먹으면 에디터에서 오류가 난다.
만약 컴포넌트에서 무엇이 필요한지 깜빡했을때에는???
커서를 컴포넌트 위에 올리거나, 컴포넌트의 props를 설정하는 부분에서 Ctrl + Space를 눌러본다.

useState 관리

...
function Counter() {
  const [count, setCount] = useState<number>(0);
  const onIncrease = () => setCount(count + 1);
  const onDecrease = () => setCount(count - 1);
  return (
    <div>
      <h1>{count}</h1>
      <div>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  )
}

useState()와 같이 제너릭을 사용하여 상태가 어떤 타입을 가지고 있을지 설정한다. (알아서 타입유추를 해주긴 함)

type Information = { name: string; description: string }
const [info, setInformation] = useState<Information | null>(null)

상태의 타입이 까다로운 구조를 가진 객체이거나 배열일 때는 제너릭을 명시하는 것이 좋다

type Todo = {id: number: text: string; done: boolean};
const [todos, setTodos] = useState<Todo[]>([]);
// .... 아니라면?
const [todos, setTodos] = useState([] as Todo[]);

배열인 경우 위와 같이 빈 배열만 넣었을 때 해당 배열이 어떤 타입으로 이루어진 배열인지 추론 할 수 없기에 제너릭을 명시한다.
아래있는 as는 Type Assertion 이라는 문법인데, 특정 값이 특정 타입이다라는 정보를 덮어 쓸 수 있는 문법.

인풋 상태 및 이벤트 다루기

...
type MyFormProps = {
  onSubmit: (form: {name: string; description: string}) => void;
}

function MyForm({onSubmit}: MyFormProps) {
  const [form, setForm] = useState({
    name: '',
    description: ''
  });

  const {name, description} = form;

  const onChange = (e: any) => {
    //일단 모르니깐 any
  }

  const handleSubmit = (e:any) => {

  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={name} onChange={onChange} />
      <input name="description" value={description} onChange={onChange} />
      <button type="submit">등록</button>
    </form>
  )
}
...

여기서 e 이벤트 객체의 타입이 무엇인지 모르겠다면… 커서를 onChange에 올려본다 e객체의 타입을 React.ChangeEvent로 지정한다.
App.tsx

const App: React.FC = () => {
  const onSubmit = (form: { name: string; description: string }) => {
    console.log(form)
  }
  return <MyForm onSubmit={onSubmit} />
}

useReducer와 사용하기

type Action = { type: 'INCREASE' } | { type: 'DECREASE' } // |를 사용하여 쭉 나열
function reducer(state: number, action: Action): number {
  switch (action.type) {
    case 'INCREASE':
      return state + 1
    case 'DECREASE':
      return state - 1
    default:
      throw new Error('unhandled action')
  }

  function Counter() {
    const [count, dispatch] = useReducer(reducer, 0)
    const onIncrease = () => dispatch({ type: 'INCREASE' })
    const onDecrease = () => dispatch({ type: 'DECREASE' })
  }

  return (
    <div>
      <h1>{count}</h1>
      <div>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  )
}

리듀서 함수의 state의 타입과 리턴 타입이 동일하다. 리듀서를 만들 땐 이렇게 타입을 동일하게 하는것이 매우 중요하다,


Written by@jaeyoung-son
배운것을 기록하는 공간입니다.

GitHub