React에 TypeScript 적용하기 (2) React

LeeKyungwon·2024년 5월 23일
0

프론트엔드 

목록 보기
39/56
post-custom-banner

React와 TypeScript

트랜스 파일링

TS -> JS
JSX -> JS

타입스크립트에서 리액트를 쓸 때는 tsx 코드를 브라우저가 바로 실행할 수 있는 형태인 JS로 변환할건지(react, react-jsx) 아니면 jsx 코드는 그대로 둘 건지(preserve) 정할 수 있다.

HTML DOM

리액트를 하면서 자주 마주치는 HTML DOM

const usernameInput = document.getElementById('username'); as HTMLInputElement;
const submitButton = document.getElementById('submit'); as HTMLButtonElement

usernameInput.focus();
submitButton.addEventListener('click', handleClick);

function handleClick(e: Event) {
  e.preventDefault();
  const message = `${usernameInput.value}님 반갑습니다!`;
  alert(message);
}

HTML DOM 노드에 대한 타입은 보편적으로 쓸 수 있는 HTMLElement가 있고, HTML태그이름Element가 있다.

이벤트에 타입을 지정해줘야 하는데 HTML에서 보편적으로 사용되는 Event라는 것이 있다.
간단하게 사용할 때는 이 타입을 써도 상관없다.
UIEvent : 클릭이나 텍스트 이벤트 같은 것들
MouseEvent, InputEvent 같은 것들도 있다.

이벤트를 인라인으로 정리하는 경우에는 타입을 추론할 수 있기 때문에 따로 타입을 지정해주지 않아도 된다.

usernameInput.focus();
submitButton.addEventListener('click', function(e){
  
});

컴포넌트 타입 정하기

리액트에서는 children으로 쓸 수 있는 타입이 정해져있다. children에는 ReactNode를 사용해야 한다.

import styles from './Button.module.css';
import { ReactNode } from 'react';

interface Props {
  className ?: string;
  id?: string;
  children?: ReactNode;
  onClick: any;
}

export default function Button({ className = '', id, children, onClick }:Props) {
  const classNames = `${styles.button} ${className}`;
  return <button className={classNames} {...rest} />;
}

화살표 함수로 컴포넌트를 만드는 경우

const Button = ({ className = '', id, children, onClick }:Props) {
  const classNames = `${styles.button} ${className}`;
  return <button className={classNames} {...rest} />;
}

아래 Input 컴포넌트는 HTML의 기본 속성들을 많이 쓰고 있어, 프롭들을 전부 지정하기는 어렵다

      <Input
        id="username"
        name="username"
        type="text"
        placeholder={t('email or phone number')}
        value={values.username}
        onChange={handleChange}
      />
import styles from './Input.module.css';

interface Props extends InputHTMLAttributes<HTMLInputElement> {}
  
export default function Input({ className = '', ...rest }:Props) {
  const classNames = `${styles.input} ${className}`;
  return <input className={classNames} {...rest} />;
}

HTMLAttributes가 뒤에 붙은 타입들은 특정 태그의 속성들을 타입으로 만들어 놓은 것을 말한다.
이런식으로 HTML에서 쓰는 기본 프롭들을 가져올 수 있다.

Hook

useState

useState를 쓸 때는 기본 값만 잘 써주면 타입 추론이 잘 된다.

const [values, setValues] = useState<{
  username: string;
  password: string;
}>({
    username: '',
    password: '',
  });

타입을 명시적으로 정해주고 싶다면 useState 뒤에 제네릭으로 써주면 된다.

타입 정의가 필요한 경우는, 문자열 배열을 state로 만든다고 했을 때, 빈 배열을 초기값으로 쓰면 타입 추론을 할 수 없으므로 타입이 never라고 추론 된다. 그러므로 이런 경우에 제네릭으로 명시적으로 타입을 지정해주어야 한다.

const [names, setNames] = useState<string[]>([]);

useRef

Ref로 HTML DOM 노드를 가져와서 쓸거라면 제네릭으로 HTML DOM 노드에 해당하는 타입을 적어주면 된다.
초기값이 비어있으면 추론된 ref 객체의 타입과 ref를 사용하는 곳의 타입이 맞지 않기 때문에 초기값으로 null을 지정해줘야 한다.

 const formRef = useRef<HTMLFormElement>(null);

useEffect

useEffect는 보통 타입을 정의할 일이 없다.

  useEffect(() => {
    const form: any = formRef.current;
    if (form) form['username'].focus();
  }, []);

이벤트 핸들러

HTML 이벤트 타입과 마찬가지로 ChangeEvent, MouseEvent 같이 -Event로 끝나는 타입을 사용한다.
제네릭으로 DOM 노드 타입을 지정해 주면 이벤트 타겟의 타입을 지정할 수 있다.
이때 주의할 점은 순수 HTML 자바스크립트에서 사용하는 MouseEvent가 아니라 react 패키지에서 불러와서 사용하는 MouseEvent 타입이라는 점이다.

어떤 이벤트인지 타입을 구체적으로 지정할 필요가 없는 경우라면 SyntheticEvent라는 타입을 사용하면 된다.

target을 사용하고 있으므로 <HTMLInputElement> 추가

import {ChangeEvent, MouseEvent} from 'react';

  function handleChange(e: ChangeEvent<HTMLInputElement>) {
    const { name, value } = e.target;
    const nextValues = {
      ...values,
      [name]: value,
    };
    setValues(nextValues);
  }

  function handleClick(e: MouseEvent) {
    e.preventDefault();

    const message = `${values.username}님 환영합니다`;
    alert(message);
  }

Button.tsx

onClick: (e: MouseEvent) => void;
onClick: MouseEventHandler<HTMLButtonElement>;

이런식으로 타입을 적어줄 수 있다.

Context

컨텍스트 값의 타입을 제네릭으로 잘 지정해주면 된다. (초깃값도 올바르게 지정해야 함)

import { createContext, useContext, useState } from 'react';

type Locale = 'ko | 'en';

interface LocaleContextValue = {
  locale: Locale;
  setLocale: (value: Locale) => void;
});

const LocaleContext = createContext<LocaleContextValue>({
  locale: 'ko',
  setLocale: () => {},
});

export function LocaleContextProvider({ children }: {children: ReactNode;}) {
  const [locale, setLocale] = useState<Locale>('ko');

  return (
    <LocaleContext.Provider
      value={{
        locale,
        setLocale,
      }}
    >
      {children}
    </LocaleContext.Provider>
  );
}

const dict = {
  ko: {
    signin: '로그인',
    username: '아이디',
    'email or phone number': 'Email 또는 전화번호',
    password: '비밀번호',
    'forgot your password?': '비밀번호를 잊으셨나요?',
    'new user?': '회원이 아니신가요?',
    signup: '가입하기',
  },
  en: {
    signin: 'Signin',
    username: 'Username',
    'email or phone number': 'Email or phone number',
    password: 'Password',
    'forgot your password?': 'Forgot your password?',
    'new user?': 'New user?',
    signup: 'Signup',
  },
};

function useLocale() {
  const { locale } = useContext(LocaleContext);
  return locale;
}

export function useSetLocale() {
  const { setLocale } = useContext(LocaleContext);
  return setLocale;
}

export function useTranslate() {
  const locale = useLocale();
  const t = (keyL keyof typeof dict[Locale]) => dict[locale][key];
  return t;
}

Next.js Pages Router 타입

공식 문서

커스텀 App

App이라는 컴포넌트의 Props 형태는 정해져 있다.
AppProps라는 타입을 next/app 패키지에서 불러와서 아래처럼 사용하면 된다.

import Head from 'next/head';
import { AppProps } from 'next/app';
import Header from '@/components/Header';
import '@/styles/global.css';

export default function App ({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <title>Codeitmall</title>
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Header />
      <Component {...pageProps} />
    </>
  );
}

프리 렌더링

정적 생성

Next.js에선 빌드 시점에 리액트 코드를 미리 렌더링해 둘 수 있는데 이것을 정적 생성이라고 한다.

import Image from 'next/image';

export async function getStaticPaths () {
  const res = await fetch('https://learn.codeit.kr/api/codeitmall/products/');
  const data = await res.json();
  const products = data.results;
  const paths = products.map((product) => ({
    params: { id: String(product.id) },
  }));

  return {
    paths,
    fallback: true,
  };
};

export async function getStaticProps(context) {
  const productId = context.params ['id'];

  let product;
  try {
    const res = await fetch(`https://learn.codeit.kr/api/codeitmall/products/${productId}`);
    const data = await res.json();
    product = data;
  } catch {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      product,
    },
  };
}

export default function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <Image
        src={product.imgUrl}
        width="480"
        height="480"
        alt={product.name}
      />
    </div>
  );
}

서버사이드 렌더링

Next.js 서버에 리퀘스트가 들어올 때마다 렌더링을 해서 보내주는 서버사이드 렌더링의 경우에도 비슷한 방식으로 해주면 된다. 아래 코드는 앞에서 보았던 예시에서 서버사이드 렌더링으로 바꾼 코드이다.

import Image from 'next/image';

export async function getServerSideProps(context) {
  const productId = context.params['id'];

  let product;
  try {
    const res = await fetch(`https://learn.codeit.kr/api/codeitmall/products/${productId}`);
    const data = await res.json();
    product = data;
  } catch {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      product,
    },
  };
}

export default function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <Image
        src={product.imgUrl}
        width="480"
        height="480"
        alt={product.name}
      />
    </div>
  );
}

getServerSideProps() 함수의 타입은 화살표 함수로 만든 다음에 GetServerSideProps로 지정하면 되고, 마찬가지로 Props 타입도 정의한 다음, 아래처럼 제네릭으로 지정하고, 페이지 컴포넌트에도 정의하면 된다.

interface Props {
  product: Product;
}

export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
  // ...
  return {
    props: {
      product,
    },
  };
};

export default function ProductPage({ product }: Props) {
  return (
    <div>
      <h1>{product.name}</h1>
      <Image
        src={product.imgUrl}
        width="480"
        height="480"
        alt={product.name}
      />
    </div>
  );
}

API 라우트

import type { NextApiRequest, NextApiResponse } from 'next'
 
export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // ...
}
post-custom-banner

0개의 댓글