리액트로 데이터 다루기 (3) 입력 폼 다루기

LeeKyungwon·2024년 4월 12일
0

프론트엔드 

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

리액트에서 입력 폼 만들기

리액트에서는 주로 인풋의 값을 state로 관리한다.
state 값과 인풋의 값을 동일하게 만드는게 핵심

리액트에서 onChange는 html의 onInput처럼 사용자가 값을 입력할 때마다 onChange 이벤트가 발생한다.
(html과 다른 점)

import { useState } from "react";
import "./ReviewForm.css";

function ReviewForm() {
  const [title, setTitle] = useState("");
  const [rating, setRating] = useState(0);
  const [content, setContent] = useState("");

  const handleTitleChange = (e) => {
    setTitle(e.target.value);
  };

  const handleRatingChange = (e) => {
    const nextRating = Number(e.target.value);
    setRating(nextRating);
  };

  const handleContentChange = (e) => {
    setContent(e.target.value);
  };

  return (
    <form className="ReviewForm">
      <input value={title} onChange={handleTitleChange} />
      <input type="number" value={rating} onChange={handleRatingChange} />
      <textarea value={content} onChange={handleContentChange} />
    </form>
  );
}

export default ReviewForm;

htmlFor

태그에서 사용하는 속성인 for 는 자바스크립트 반복문 키워드인 for 와 겹치기 때문에 리액트에서는 htmlFor 를 사용한다.

onSubmit

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log({
      title,
      rating,
      content,
    });
  };

submit 버튼을 눌렀을 때 기본 동작은 submit과 함께 getRequest를 보내는 것이다. (페이지 이동)
그렇기 때문에 e.preventDefault();를 통해 랜더링을 막는다.

하나의 state로 폼 구현하기

import { useState } from 'react';
import './ReviewForm.css';

function ReviewForm() {
  const [values, setValues] = useState({
    title: '',
    rating: 0,
    content: '',
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log(values);
  };

  return (
    <form className="ReviewForm" onSubmit={handleSubmit}>
      <input name="title" value={values.title} onChange={handleChange} />
      <input type="number" name="rating" value={values.rating} onChange={handleChange} />
      <textarea name="content" value={values.content} onChange={handleChange} />
      <button type="submit">확인</button>
    </form>
  );
}

export default ReviewForm;  

깔끔한 코드를 작성할 수 있다.

제어 컴포넌트와 비제어 컴포넌트

제어 컴포넌트

인풋의 value 값을 리액트에서 지정
리액트에서 사용하는 값과 실제 인풋 value 의 값이 항상 일치
값을 예측하기가 쉽고 인풋에 쓰는 값을 여러 군데서 쉽게 바꿀 수 있다는 장점이 있어서 주로 권장되는 방법

이때 State냐 Prop이냐는 중요하지 않고, 리액트로 value 를 지정한다는 것이 핵심

예시

function TripSearchForm() {
  const [values, setValues] = useState({
    location: 'Seoul',
    checkIn: '2022-01-01',
    checkOut: '2022-01-02',
  })

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues((prevValues) => ({
      ...prevValues,
      [name]: value,
    }));
  }
    
  return (
    <form>
      <h1>검색 시작하기</h1>
      <label htmlFor="location">위치</label>
      <input id="location" name="location" value={values.location} placeholder="어디로 여행가세요?" onChange={handleChange} />
      <label htmlFor="checkIn">체크인</label>
      <input id="checkIn" type="date" name="checkIn" value={values.checkIn} onChange={handleChange} />
      <label htmlFor="checkOut">체크아웃</label>
      <input id="checkOut" type="date" name="checkOut" value={values.checkOut} onChange={handleChange} />
      <button type="submit">검색</button>
    </form>
  )
}
function TripSearchForm({ values, onChange }) {
  return (
    <form>
      <h1>검색 시작하기</h1>
      <label htmlFor="location">위치</label>
      <input id="location" name="location" value={values.location} placeholder="어디로 여행가세요?" onChange={onChange} />
      <label htmlFor="checkIn">체크인</label>
      <input id="checkIn" type="date" name="checkIn" value={values.checkIn} onChange={onChange} />
      <label htmlFor="checkOut">체크아웃</label>
      <input id="checkOut" type="date" name="checkOut" value={values.checkOut} onChange={onChange} />
      <button type="submit">검색</button>
    </form>
  )
}

비제어 컴포넌트

인풋의 value 값을 리액트에서 지정하지 않음
경우에 따라서 필요한 방법 (파일 인풋)
리액트에서 실제 인풋값을 제어하지 않는 경우

function TripSearchForm({ onSubmit }) {
  return (
    <form onSubmit={onSubmit} >
      <h1>검색 시작하기</h1>
      <label htmlFor="location">위치</label>
      <input id="location" name="location" placeholder="어디로 여행가세요?" />
      <label htmlFor="checkIn">체크인</label>
      <input id="checkIn" type="date" name="checkIn" />
      <label htmlFor="checkOut">체크아웃</label>
      <input id="checkOut" type="date" name="checkOut" />
      <button type="submit">검색</button>
    </form>
  )
}

위 코드에서 폼 태그를 참조하는 방법

const handleSubmit = (e) => {
  e.preventDefault();
  const form = e.target;
  const location = form['location'].value;
  const checkIn = form['checkIn'].value;
  const checkOut = form['checkOut'].value;
  // ....
}
const handleSubmit = (e) => {
  e.preventDefault();
  const form = e.target;
  const formData = new FormData(form);
  // ...
}

파일 인풋

리액트에서 파일 인풋은 반드시 비제어 인풋으로 지정해야 한다.

function FileInput({ name, value, onChange }) {
  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  return <input type="file" onChange={handleChange} />;
}

export default FileInput;

useRef

생성하고 DOM 노드에 연결하기

const ref = useRef();

// ...

return <div ref={ref}>안녕 리액트!</div>;

DOM 노드 참조하기

const node = ref.current;
if (node) {
  // node를 사용하는 코드
}

ref로 DOM 노드 가져오기

function FileInput({ name, value, onChange }) {
  const inputRef = useRef();
  
  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  return <input type="file" onChange={handleChange} ref={inputRef} />;
}

export default FileInput;

ref로 실제 DOM 노드를 참조할 수 있다.

파일 인풋 초기화

import { useRef } from 'react';

function FileInput({ name, value, onChange }) {
  const inputRef = useRef();

  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  const handleClearClick = () => {
    const inputNode = inputRef.current;
    if (!inputNode) return;

    inputNode.value = '';
    onChange(name, null);
  };

  return (
    <div>
      <input type="file" onChange={handleChange} ref={inputRef} />
      {value && <button onClick={handleClearClick}>X</button>}
    </div>
  );
}

export default FileInput;

ref, useRef 사용하기

Ref 객체 생성

import { useRef } from 'react';

// ...

const ref = useRef();

useRef 함수로 Ref 객체를 만들 수 있다.

ref Prop 사용하기

const ref = useRef();

// ...

<div ref={ref}> ... </div>

ref Prop에다가 앞에서 만든 Ref 객체를 내려주면 된다.

Ref 객체에서 DOM 노드 참조하기

const node = ref.current;
if (node) {
  // node 를 사용하는 코드
}

Ref 객체의 current 라는 프로퍼티를 사용하면 DOM 노드를 참조할 수 있다.
current 값은 없을 수도 있으니까 반드시 값이 존재하는지 검사하고 사용해야 함!

이미지 파일 미리보기

파일 객체를 object Url로 만들면 된다.

import { useEffect, useRef, useState } from 'react';

function FileInput({ name, value, onChange }) {
  const [preview, setPreview] = useState();
  const inputRef = useRef();

  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  const handleClearClick = () => {
    const inputNode = inputRef.current;
    if (!inputNode) return;

    inputNode.value = '';
    onChange(name, null);
  };

  useEffect(() => {
    if (!value) return;
    const nextPreview = URL.createObjectURL(value);
    setPreview(nextPreview);
  }, [value]);

  return (
    <div>
      <img src={preview} alt="이미지 미리보기" />
      <input type="file" accept="image/png, image/jpeg" onChange={handleChange} ref={inputRef} />
      {value && <button onClick={handleClearClick}>X</button>}
    </div>
  );
}

export default FileInput;

preview state 추가

사이드 이펙트 정리하기

메모리 할당을 해제하는 것 (revokeObjectURL 함수 사용)

import { useEffect, useRef, useState } from 'react';

function FileInput({ name, value, onChange }) {
  const [preview, setPreview] = useState();
  const inputRef = useRef();

  const handleChange = (e) => {
    const nextValue = e.target.files[0];
    onChange(name, nextValue);
  };

  const handleClearClick = () => {
    const inputNode = inputRef.current;
    if (!inputNode) return;

    inputNode.value = '';
    onChange(name, null);
  };

  useEffect(() => {
    if (!value) return;
    const nextPreview = URL.createObjectURL(value);
    setPreview(nextPreview);

    return () => {
      setPreview();
      URL.revokeObjectURL(nextPreview);
    };
  }, [value]);

  return (
    <div>
      <img src={preview} alt="이미지 미리보기" />
      <input type="file" accept="image/png, image/jpeg" onChange={handleChange} ref={inputRef} />
      {value && <button onClick={handleClearClick}>X</button>}
    </div>
  );
}

export default FileInput;

사이드 이펙트와 useEffect

프로그래밍에서 사이드 이펙트는 외부에 부수적인 작용을 하는 것을 말한다.
(함수 안에서 함수 바깥에 있는 값이나 상태를 변경하는 것, ex)console.log)

useEffect는 리액트 컴포넌트 함수 안에서 사이드 이펙트를 실행하고 싶을 때 사용하는 함수이다.
주로 외부에 있는 데이터나 상태를 변경할 때 사용

useEffect는 동기화에 쓰면 유용한 경우가 많다. (컴포넌트 안에 데이터와 리액트 바같에 있는 데이터를 일치시키는 것)

정리 함수

useEffect(() => {
  // 사이드 이펙트

  return () => {
    // 사이드 이펙트에 대한 정리
  }
}, [dep1, dep2, dep3, ...]);

useEffect 의 콜백 함수에서 사이드 이펙트를 만들면 정리가 필요한 경우가 있다.
이럴 때 콜백 함수에서 리턴 값으로 정리하는 함수를 리턴할 수 있는데, 리턴한 정리 함수에서는 사이드 이펙트에 대한 뒷정리를 한다.

예를 들면 이미지 파일 미리보기를 구현할 때 Object URL을 만들어서 브라우저의 메모리를 할당(createObjectURL) 하고 정리 함수에서는 이때 할당한 메모리를 다시 해제(revokeObjectURL)한 경우가 있다.

콜백을 한 번 실행했으면 정리 함수도 반드시 한 번 실행된다.

정확히는 새로운 콜백 함수가 호출되기 전에 실행되거나 (앞에서 실행한 콜백의 사이드 이펙트를 정리), 컴포넌트가 화면에서 사라지기 전에 실행된다. (맨 마지막으로 실행한 콜백의 사이드 이펙트를 정리).

post-custom-banner

0개의 댓글