[강의] 입력 폼 다루기

김하은·2023년 11월 27일
0

코드잇 강의 정리

목록 보기
50/60

리액트에서 입력 폼 만들기

  • 제어 컴포넌트: 리액트에서는 인풋 값을 주로 state로 다루는데, 인풋 값과 state 값을 동일하게 만드는 게 핵심임

HTML과 다른 점

  • onChange
    • 순수 HTML과 다르게 onChange Prop을 사용하면 입력 값이 바뀔 때마다 핸들러 함수를 실행함
    • oninput 이벤트와 같다고 생각하됨
    • 리액트 개발자들은 주로 onChange 라는 Prop을 사용함
  • htmlFor
    • <label /> 태그에서 사용하는 속성인 for 는 자바스크립트 반복문 키워드인 for 와 겹치기 때문에 리액트에서는 htmlFor 를 사용함

폼을 다루는 기본적인 방법

  • 스테이트를 만들고 target.value 값을 사용해서 값을 변경해 줄 수 있음
  • 이때 value Prop으로 스테이트 값을 내려주고, onChange Prop으로 핸들러 함수를 넘겨줌
function TripSearchForm() {
  const [location, setLocation] = useState('Seoul');
  const [checkIn, setCheckIn] = useState('2022-01-01');
  const [checkOut, setCheckOut] = useState('2022-01-02');

  const handleLocationChange = (e) => setLocation(e.target.value);

  const handleCheckInChange = (e) => setCheckIn(e.target.value);

  const handleCheckOutChange = (e) => setCheckOut(e.target.value);
    
  return (
    <form>
      <h1>검색 시작하기</h1>
      <label htmlFor="location">위치</label>
      <input id="location" name="location" value={location} placeholder="어디로 여행가세요?" onChange={handleLocationChange} />
      <label htmlFor="checkIn">체크인</label>
      <input id="checkIn" type="date" name="checkIn" value={checkIn} onChange={handleCheckInChange} />
      <label htmlFor="checkOut">체크아웃</label>
      <input id="checkOut" type="date" name="checkOut" value={checkOut} onChange={handleCheckOutChange} />
      <button type="submit">검색</button>
    </form>
  )
}

폼 값을 객체 하나로 처리하기

  • 이벤트 객체의 target.name 과 target.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>
  )
}

기본 submit 동작 막기

  • HTML 폼의 기본 동작은 submit 타입의 버튼을 눌렀을 때 페이지를 이동하는 것이지만 이벤트 객체의 preventDefault 를 사용하면 이 동작을 막을 수 있음
const handleSubmit = (e) => {
  e.preventDefault();
  // ...
}

제어 컴포넌트

  • 인풋 태그의 value 속성을 지정하고 사용하는 컴포넌트
  • 리액트에서 인풋의 값을 제어하는 경우로 리액트에서 지정한 값과 실제 인풋 value 의 값이 항상 같음
  • 값을 예측하기가 쉽고 인풋에 쓰는 값을 여러 군데서 쉽게 바꿀 수 있다는 장점이 있음
  • 이때 State냐 Prop이냐는 중요하지 않고, 리액트로 value 를 지정한다는 것이 핵심임
//예시 1
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>
  )
}

//예시 2
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>
  )
}
  • 위처럼 작성해도 onSubmit 함수에서는 폼 태그를 참조할 수 있음
  • FormData: form필드와 그 값을 쉽게 보내고 생성할 수 있도록 도와주는 객체로 FormData 객체는 HTML 폼 데이터를 나타냄
// 이벤트 객체의 target 활용
const handleSubmit = (e) => {
  e.preventDefault();
  const form = e.target;
  const location = form['location'].value;
  const checkIn = form['checkIn'].value;
  const checkOut = form['checkOut'].value;
  // ....
}

// FormData 만들기
const handleSubmit = (e) => {
  e.preventDefault();
  const form = e.target;
  const formData = new FormData(form);
  // ...
}

파일 인풋

  • 파일 인풋에서는 이벤트 객체의 target.value 값이 아니라 target.files를 사용함
  • 파일 인풋은 반드시 비제어 인풋으로만 사용해야 함
  • 파일 인풋의 value 속성은 사용자만 직접 바꿀 수 있으며 자바스크립트로 바꿀 때는 빈 문자열로만 바꿀 수 있음

ref와 useRef

Ref 객체 생성

  • useRef 함수로 Ref 객체를 만들 수 있음
import { useRef } from 'react';

// ...

const ref = useRef();

ref Prop 사용하기

  • ref Prop에다가 앞에서 만든 Ref 객체를 내려주면 됩
const ref = useRef();

// ...

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

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

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

예시: 이미지 크기 구하기

import { useRef } from 'react';

function Image({ src }) {
  const imgRef = useRef();

  const handleSizeClick = () => {
    const imgNode = imgRef.current;
    if (!imgNode) return;

    const { width, height } = imgNode;
    console.log(`${width} x ${height}`);
  };

  return (
    <div>
      <img src={src} ref={imgRef} alt="크기를 구할 이미지" />
      <button onClick={handleSizeClick}>크기 구하기</button>
    </div>
  );
}

사이드 이펙트와 useEffect

사이드 이펙트(Side Effect)란?

  • 사이드 이펙트: 프로그래밍에선 말 그대로 외부에 부수적인 작용을 하는 걸 말함
  • 함수 안에서 함수 바깥에 있는 값이나 상태를 변경하는 것
// 예시
let count = 0;

function add(a, b) {
  const result = a + b;
  count += 1; // 함수 외부의 값을 변경
  return result;
}

const val1 = add(1, 2);
const val2 = add(-4, 5);
  • add 함수는 실행하면서 함수 외부의 상태(count 변수)가 바뀌기 때문에 이런 함수를 "사이드 이펙트가 있다"고 함
  • console.log 함수를 사용하면 값을 계산해서 리턴하는 게 아니라 브라우저 콘솔 창에 문자열을 출력 하므로 외부 상태를 변경해서 문자열을 출력하는 것으로 사이드 이펙트가 있음

사이드 이펙트와 useEffect

  • useEffect 는 리액트 컴포넌트 함수 안에서 사이드 이펙트를 실행하고 싶을 때 사용하는 함수
  • 예를들면 DOM 노드를 직접 변경한다거나, 브라우저에 데이터를 저장하고, 네트워크 리퀘스트를 보내는 것이 있음
  • 주로 리액트 외부에 있는 데이터나 상태를 변경할 때 사용함
// 페이지 정보 변경
useEffect(() => {
  document.title = title; // 페이지 데이터를 변경
}, [title]);

// 네트워크 요청
useEffect(() => {
  fetch('https://example.com/data') // 외부로 네트워크 리퀘스트
    .then((response) => response.json())
    .then((body) => setData(body));
}, [])

// 데이터 저장
useEffect(() => {
  localStorage.setItem('theme', theme); // 로컬 스토리지에 테마 정보를 저장
}, [theme]);

//타이머
useEffect(() => {
  localStorage.setItem('theme', theme); // 로컬 스토리지에 테마 정보를 저장
}, [theme]);
  • 참고로 setInterval 이라는 함수를 쓰면 일정한 시간마다 콜백 함수를 실행할 수 있음

useEffect를 쓰면 좋은 경우

  • useEffect 는 쉽게 말해서 '동기화'에 쓰면 유용한 경우가 많음
  • 여기서 동기화는 컴포넌트 안에 데이터와 리액트 바깥에 있는 데이터를 일치시키는 걸 말함
  • 아래 App 컴포넌트는 인풋 입력에 따라 페이지 제목을 바꾸는 컴포넌트임
// 핸들러 함수만 사용한 예시
import { useState } from 'react';

const INITIAL_TITLE = 'Untitled';

function App() {
  const [title, setTitle] = useState(INITIAL_TITLE);

  const handleChange = (e) => {
    const nextTitle = e.target.value;
    setTitle(nextTitle);
    document.title = nextTitle;
  };

  const handleClearClick = () => {
    const nextTitle = INITIAL_TITLE;
    setTitle(nextTitle);
    document.title = nextTitle;
  };

  return (
    <div>
      <input value={title} onChange={handleChange} />
      <button onClick={handleClearClick}>초기화</button>
    </div>
  );
}

export default App;
  • handleChange 함수와 handleClearClick 함수가 있음
  • 모두 title 스테이트를 변경한 후에 document.title 도 함께 변경해주고 있음
  • 여기서 document.title 값을 바꾸는 건 외부의 상태를 변경하는 거니까 사이드 이펙트임
  • 만약 새로 함수를 만들어서 setTitle 을 사용하는 코드를 추가할 때마다 document.title 값도 변경해야 한다는 걸 기억해뒀다가 관련된 코드를 작성해야 한다는 점이 아쉬움
import { useEffect, useState } from 'react';

const INITIAL_TITLE = 'Untitled';

function App() {
  const [title, setTitle] = useState(INITIAL_TITLE);

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

  const handleClearClick = () => {
    setTitle(INITIAL_TITLE);
  };

  useEffect(() => {
    document.title = title;
  }, [title]);

  return (
    <div>
      <input value={title} onChange={handleChange} />
      <button onClick={handleClearClick}>초기화</button>
    </div>
  );
}

export default App;
  • useEffect 를 사용한 예시에서는 document 를 다루는 사이드 이펙트 부분만 따로 처리하고 있음
  • 이렇게 하니 setTitle 함수를 쓸 때마다 document.title 을 변경하는 코드를 신경 쓰지 않아도 되니까 편리함
  • 게다가 처음 렌더링 되었을 때 'Untitled'라고 페이지 제목을 변경하는 효과까지 있음
  • 그리고 이 코드를 본 사람이라면 누구든 이 컴포넌트는 title 스테이트 값을 가지고 항상 document.title 에 반영해줄 것이라고 쉽게 예측할 수 있음
  • 이렇게 useEffect 는 리액트 안과 밖의 데이터를 일치시키는데 활용하면 좋음
  • useEffect 를 사용했을 때 반복되는 코드를 줄이고, 동작을 쉽게 예측할 수 있는 코드를 작성할 수 있음

정리 함수가 실행되는 시점

  • 쉽게 말해서 콜백을 한 번 실행했으면, 정리 함수도 반드시 한 번 실행된다고 생각하면 됨
  • 정확히는 새로운 콜백 함수가 호출되기 전에 실행되거나 (앞에서 실행한 콜백의 사이드 이펙트를 정리), 컴포넌트가 화면에서 사라지기 전에 실행됨 (맨 마지막으로 실행한 콜백의 사이드 이펙트를 정리)
// 예시: 타이머
import { useEffect, useState } from 'react';

function Timer() {
  const [second, setSecond] = useState(0);

  useEffect(() => {
    const timerId = setInterval(() => {
      console.log('타이머 실행중 ... ');
      setSecond((prevSecond) => prevSecond + 1);
    }, 1000);
    console.log('타이머 시작 🏁');

    return () => {
      clearInterval(timerId);
      console.log('타이머 멈춤 ✋');
    };
  }, []);

  return <div>{second}</div>;
}

function App() {
  const [show, setShow] = useState(false);

  const handleShowClick = () => setShow(true);
  const handleHideClick = () => setShow(false);

  return (
    <div>
      {show && <Timer />}
      <button onClick={handleShowClick}>보이기</button>
      <button onClick={handleHideClick}>감추기</button>
    </div>
  );
}

export default App;
  • 일정한 시간 간격마다 콜백 함수를 실행하는 setInterval 이라는 함수도 정리가 필요한 사이드 이펙트인데 매번 타이머를 시작하기만 하면 타이머가 엄청나게 많아지기 때문임
  • 이 컴포넌트는 렌더링이 끝나면 타이머를 시작하고, 화면에서 사라지면 타이머를 멈춤
  • 사용자가 '보이기' 버튼을 눌렀을 때 show 값이 참으로 바뀌면서 다시 렌더링 됨
  • 조건부 렌더링에 의해서 Timer 컴포넌트를 렌더링 하겠죠?

Timer 컴포넌트에서는 useEffect 에서 타이머를 시작하고, 정리 함수를 리턴합니다.

콘솔에는 '타이머 시작 🏁'이 출력됩니다.

다시 사용자가 '감추기' 버튼을 누르면 show 값이 거짓으로 바뀌면서 다시 렌더링 됩니다.

조건부 렌더링에 의해서 이제 Timer 컴포넌트를 렌더링 하지 않겠죠?

그럼 리액트에선 마지막으로 앞에서 기억해뒀던 정리 함수를 실행해줍니다.

타이머를 멈추고 콘솔에는 '타이머 멈춤 ✋'이 출력됩니다.

이런 식으로 정리 함수를 리턴하면 사이드 이펙트를 정리하고 안전하게 사용할 수 있다는 것.

꼭 기억해주세요!

profile
아이디어와 구현을 좋아합니다!

0개의 댓글