Developer

[유데미X웅진씽크빅X스나이퍼팩토리] React 2기 - 사전직무교육 5일차

yunicornlab 2024. 8. 25. 23:22
반응형

오늘 배운 건 여러 번 복습해야겠다는 생각이 들었다. 특히 Form 구성하는 연습을 많이 해보아야 겠다.

 

저번 시간에 만든 starter를 이용해서 새 프로젝트를 생성해보았다!

https://github.com/Attainy/react-ts-tailwind-starter

 

GitHub - Attainy/react-ts-tailwind-starter: React + Vite + TypeScript + TailwindCSS + Tailwind-Merge 기본 세팅

React + Vite + TypeScript + TailwindCSS + Tailwind-Merge 기본 세팅 - Attainy/react-ts-tailwind-starter

github.com

mkdir 폴더이름

cd 폴더이름

code .

git clone https://github.com/Attainy/react-ts-tailwind-starter.git .

 

React Hook - useState

React의 Hook은 클래스형 컴포넌트의 생명주기 기능을 함수형 컴포넌트에서도 사용할 수 있도록 16.8 버전 이후에 만들어진 Built-in 함수이다.

useState는 추적가능한 상태 관리를 위해 사용하는 훅이다.

useState를 사용하지 않고 일반 변수로 선언해서 사용하면, 이 값이 변경될 때 React가 이 상태 변화를 감지하지 못해서 화면에 렌더링이 되지 않는다.

그리고 이 useState는 상태값과 상태를 변경할 수 있는 setter함수를 반환해주기 때문에 배열 비구조화 문법을 이용한 표현을 주로 사용한다.

const [변수이름, set변수이름] = useState(초기값)

import { useState } from "react";

const App = () => {
  const [num, setNum] = useState(0);

  const onClickHandler = () => {
    setNum(50);
  };
  return (
    <>
      <h1>Number: {num}</h1>
      <button onClick={onClickHandler}>숫자 변경</button>
    </>
  );
};
export default App;

 

상태값을 변경할 때는 반드시 useState로 반환된 배열의 두 번째 요소인 setter 함수를 사용해서 변경해주어야 한다.

왜냐하면 리액트에서 상태값을 바꿀 때는 불변성(값이 변하지 않는 특징)을 유지하면서 변경해주어야 

리액트가 상태가 변경되었다는 사실을 인지할 수 있고, 상태 변경을 인지할 수 있어야 화면에 다시 렌더링해서 보여줄 수 있기 때문이다.

 

setState - 비동기

useState를 사용할 때는 또 다른 특징이 있다.

바로 setState 함수(useState로 반환된 배열의 두 번째 요소)가 비동기로 동작한다는 것이다.

 

만약에, setState 함수로 상태값을 50으로 바꾸고 싶으면 다음 두 가지의 방식으로 코드를 작성할 수 있다.

1) setState(50)

2) setState(() => 50)

1번 방식은 비동기로 동작하고, 2번 방식은 동기적으로 동작한다는 큰 차이가 있다.

그리고 이전 값을 참조해야 하는 경우에는 무조건 2번 방식으로 사용해야 한다.

아래와 같이 1번 방식을 여러번 작성하게 되면, 버튼을 눌렀을 때 setState가 비동기로 동작해서 동시다발적으로 실행되기 때문에,

네 번이 더해지는 게 아니라 한 번만 더해져서 최신 상태값을 보장해주지 않는다.

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  const onClickHandler = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
  };

  return (
    <>
      <h1>{count}</h1>
      <button onClick={onClickHandler}>증가</button>
    </>
  );
};
export default App;

 

하지만, 아래처럼 작성하면, 동기적으로 동작하게 되어서 네 번이 더해지면서 최신 값을 보장해준다.

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  const onClickHandler = () => {
    setCount((count) => {
      return count + 1;
    });
    setCount((count) => {
      return count + 1;
    });
    setCount((count) => {
      return count + 1;
    });
  };

  return (
    <>
      <h1>{count}</h1>
      <button onClick={onClickHandler}>증가</button>
    </>
  );
};
export default App;

 

물론, 실제로는 return을 생략하고 화살표함수로 축약해서 아래와 같이 작성한다.

import { useState } from "react";

const App = () => {
  const [count, setCount] = useState(0);

  const onClickHandler = () => {
    setCount((count) => count + 1);
    setCount((count) => count + 1);
    setCount((count) => count + 1);
    setCount((count) => count + 1);
  };

  return (
    <>
      <h1>{count}</h1>
      <button onClick={onClickHandler}>증가</button>
    </>
  );
};
export default App;

 

리액트는 불변성을 유지하는 것이 중요하기 때문에, 자바스크립트에서 새로운 배열을 반환해주는 메서드나 방법을 잘 기억해두는 것이 좋다.

  • slice : 배열을 자르는 함수
  • filter : 배열을 필터링하는 함수
  • map : 배열을 변형해서 새로운 배열로 반환하는 함수
  • reduce : 배열을 축약하는 함수
  • concat : 배열을 병합해주는 함수
  • Object.assign
  • Spread 연산자

(예시)

const App = () => {
  const [posts, setPosts] = useState([
    {
      id: 1,
      text: 게시글1,
      completed: true,
    },
  ]);
  
  setPosts((post) => [
    ...posts,
    {id: 2, text: "게시글2", completed: false}
  ])
  
  return (
    <>
      <ul>
        {posts.map((post, index) => (
          <li key={index}>{post.text}</li>
        ))}
      </ul>
    </>
  );
}
export default App;

 

 

잠시 정리하자면, 

이전 값을 참조할 필요가 없으면 setState(값)의 형태로 사용하고,

이전 값을 참조해야 한다면 setState((이전값) => 새로운 return값)의 형태로 사용하면 된다.

 

Form 구성하기

value와 onChange는 짝궁처럼 사용하는 게 좋다.

form 태그 안에서 button 태그가 사용되면 기본적으로 type이 submit으로 지정된다. 이때 이 버튼을 누르면 form 태그의 action 속성에 적힌 주소로 데이터가 전달된다.

보통 button에 onClick을 사용하지 않고, button의 타입을 submit으로 지정한 다음 form에 onSubmit을 사용한다.

import { useState } from "react";

const App = () => {
  const [email, setEmail] = useState("");

  const onClickHandler = (e: React.FormEvent<HTMLFormElement>) => {
    // 기본 이벤트 취소
    e.preventDefault();
    
    // 로직 추가
  };

  return (
    <>
      <form onSubmit={onClickHandler}>
        <input
          type="text"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Enter Email"
        />
        <button type="submit">제출</button>
      </form>
    </>
  );
};
export default App;

 

HTML 요소에서 event 객체를 전달받을 때 타입은 어떻게 정의할까?

input 태그는 e: React.ChangeEvent<HTMLInputElement>

textarea태그는 e: React.ChangeEvent<HTMLTextAreaElement>

select태그는 e: React.ChangeEvent<HTMLSelectElement>

form태그는 e: React.FormEvent<HTMLFormElement>로 정의해주면 된다.

 

각각 useState를 사용해 상태변수를 선언하고, value와 onChange로 연결해주면 되는데,

체크박스에서 checked 속성 같은 경우에는 event 객체를 사용하지 않아도 되기 때문에

const [checked, setChecked] = useState(false);

const onChangeChecked = () => setChecked((checked) => !checked);

이렇게 선언해두고

<input type="checkbox" checked={checked} onChange={onChangeChecked} />

이렇게 태그를 만들어서 사용하면 된다.

 

하지만, 모든 입력 요소마다 상태변수와 onChange 함수를 선언해야 하면 너무 길어지고 비효율적이다.

이를 효율적으로 처리하는 방법을 알아보자.

 

1. 객체로 정의해서 일괄적으로 처리하는 방법

객체를 정의하고, 값이 수정될때마다 객체 값이 수정된 새로운 객체로 교체해주는 방법이다.

import React, { useState } from "react";

const App = () => {
  const [formState, setFormState] = useState({
    email: "",
    password: "",
    desc: "",
  });

  const onChangeFormState = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
  ) => {
    setFormState((formState) => ({
      ...formState,
      [e.target.name]: e.target.value,
    }));
  };

  return (
    <>
      <pre>{JSON.stringify(formState, null, 2)}</pre>
      <input
        type="email"
        name="email"
        value={formState.email}
        onChange={onChangeFormState}
      />
      <input
        type="password"
        name="password"
        value={formState.password}
        onChange={onChangeFormState}
      />
      <textarea name="desc" value={formState.desc}></textarea>
    </>
  );
};
export default App;

 

2. Custom Hook을 만들어서 사용하는 방법

src 폴더 내에 hooks라는 폴더를 만든 후에 useInput.ts라는 파일을 만들어서 다음과 같이 작성해보자.

import { useState } from "react";

type UseInputReturn = [
  string,
  (e: React.ChangeEvent<HTMLInputElement>) => void
];

function useInput(initialValue: string): UseInputReturn {
  const [value, setValue] = useState(initialValue);
  const onChangeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  };
  return [value, onChangeValue];
}
export default useInput;

 

이후, App.tsx를 다음과 같이 수정해서 더 간편하게 작성할 수 있다.

import useInput from "./hooks/useInput";

const App = () => {
  // Email
  const [email, onChangeEmail] = useInput("");
  // Password
  const [password, onChangePassword] = useInput("");

  return (
    <>
      <pre>{JSON.stringify({ email, password, name, date }, null, 2)}</pre>
      <input type="email" value={email} onChange={onChangeEmail} />
      <input type="password" value={password} onChange={onChangePassword} />
    </>
  );
};
export default App;

 

값을 Reset하는 함수도 포함시켜서 작성할 수도 있다.

 

React Hook - useRef

useRef는 특정 DOM(문서 객체 모델) 요소나 값을 참조할 때 사용하는 훅으로, useState와 달리 리액트가 상태 변화를 감지하지 못하기 때문에 재렌더링이 발생하지 않는다.

그래서 성능을 위해 useState 대신에 useRef를 사용하는 것이 좋은데, 실제로 쓸 일을 생각보다 많지 않다.

useState로 선언한 변수의 값을 setState함수로 변경하면, 리액트가 상태 변화를 감지하면서 렌더링이 다시 일어나게 되는데,

이때 상태가 변경되는 컴포넌트를 시작으로, 연결된 모든 자식 컴포넌트가 다시 렌더링 된다.

물론 실제로 DOM에는 변경된 부분만 바뀌므로 그 부분만 영향이 있어보이지만, 그 과정에서 전체를 다시 검사하고 바꾸려고 시도한다.

그렇기 때문에, 변경될 상태값이 있는 컴포넌트는 가능한 하위 컴포넌트로 배치시키는 것이 좋다. 

정말 중요한 원칙이지만 지키기 어려운 규칙이기도 하다.

 

useRef를 사용하면 문서 객체를 참조할 수 있기 때문에 문서 객체를 다룰 수 있는 자바스크립트의 메서드를 함께 사용할 수 있다.

그 중 대표적으로 focus가 있다.

import { useRef } from "react";

const Auth = () => {
  const els = useRef<HTMLInputElement>(null);
  
  const onSubmitHandler = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    
    // current만 있을 때만 focus를 사용하라는 의미
    els.current?.focus();
  };

  return (
    <>
      <input ref={els} />
    </>
  );
};
export default Auth;

 

그런데 이렇게 대놓고 input 태그만 있을 때는 바로 ref를 지정하면 되기 때문에 사용하기 쉽다.

문제는 input을 따로 컴포넌트로 만들었을 때다!

컴포넌트로 만든 Input에 ref={els}를 하면, 이땐 ref가 지정되는 의미가 아니라 props로 넘겨주는 의미가 되어버린다.

props로 ref를 넘겨줄 때는 안타깝게도 아주 까다로운 조작이 필요하다.

React 19에서는 개선이 될 예정이라고는 하지만, 아직은 이 방법밖에 없다고 한다.

import React, { forwardRef } from 'react';

type TInputProps = Omit<React.ComponentPropsWithRef<"input">, "type"> & {
  type: "text" | "password" | "email" | "number";
};

// forwardRef를 사용하여 ref를 props로 받는 Input 컴포넌트 정의
const Input = forwardRef<HTMLInputElement, TInputProps>((props: TInputProps, ref) => {
  return <input ref={ref} {...props} />;
});

Input.displayName = "Input";
export default Input;

 

1. React.ComponentPropsWithoutRef -> React.ComponentPropsWithRef로 수정

2. 함수를 () 괄호로 감싸주기

3. 감싼 괄호() 앞에 forwardRef<HTMLInputElement, 정의한 Type> 작성해주기

4. 두 번째 인자로 ref 받기

5. Input.displayName = 이름 (이걸 안해주면 이름을 못찾아서 build에서 에러가 난다.)

 

 

TIP

VSCode Auto Import

VSCode에서 import할 이름을 선택하고, Ctrl + . 를 누르면 자동으로 import 구문을 생성해주는 기능을 사용할 수 있다!

짱 편하다.

 

Chrome Extension - Screen Ruler

https://chromewebstore.google.com/detail/screen-ruler-measure-the/jfbbgijjljfbolelfkopkhbfjajjampm?pli=1

 

Screen Ruler - Measure The Web - Chrome 웹 스토어

Measure sizes, distances, margins and paddings of any element on any web page.

chromewebstore.google.com

 

TypeScript로 선언 시 타입 추론되지 않는 것 위주로 작성하기

const [count, setCount] = useState(10);과 같은 코드는 10을 통해서 number 타입으로 추론이 되기 때문에 

굳이 const [count, setCount] = useState<string>(10);  이렇게 작성할 필요가 없다.

하지만, const [arr, setArr] = useState([]);  이렇게만 작성하면 배열 안에 어떤 타입의 요소가 올 지 모르기 때문에

const [arr, setArr] = useState<string []>([]); 이런 식으로 배열의 요소 타입을 제네릭으로 작성해주어야 한다.

 

대소문자 종류를 유지하면서 동시에 변경하는 Extension

VSCode에서 Ctrl + D로 문자열을 동시에 바꿀 때, 기존 대소문자를 유지하면서 바꿔주는 꿀 Extension이다.

반응형