본문 바로가기

react

[코드잇] 리액트로 데이터 다루기(3)

1. 리액트에서 입력 폼 만들기 

 

- 리액트에서는 주로 input의 값을 State로 관리

- State값과 input의 값을 동일하게 만듬

- 리액트에서의 onChange는 순수 자바스크립트에서 onChange와 다르게 동작

 -> 사용자가 값을 입력할 때마다 onChange 이벤트 발생

 

ReviewForm.js

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;

 

App.js

return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewForm />
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      {hasNext && (<button disabled={isLoading} onClick={handleLoadMore}>더보기</button>)}
      {loadingError?.message && <span>{loadingError.message}</span>}
    </div>
  );

 

 

 

2. onSubmit

 

- HTML form 태그의 기본 동작은 submint 버튼을 눌렀을 때 입력 폼의 값과 함께 GET 리퀘스트를 보내는 것

- 이러한 기본동작을 막아줘야 함

- 이벤트 객체의 preventDefault 함수 사용

 

ReviewForm.js

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

  return (
    <form className="ReviewForm" onSubmit={handleSubmit}>
      <input value={title} onChange={handleTitleChange} />
      <input type="number" value={rating} onChange={handleRatingChange} />
      <textarea value={content} onChange={handleContentChange}/>
      <button type="submit">확인</button>
    </form>

 

3. 하나의 state로 폼 구현

 

- 이벤트 객체에서 name값을 가져 올 수 있다는 점을 활용

 

ReviewForm.js

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;

 

 

 

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

 

제어 컴포넌트(Controlled Component)

- input의 value값을 리액트에서 지정 

- input의 value와 onChange 핸들러를 사용하면 input의 값 제어 가능

- 리액트에서 사용하는 값과 실제 input의 값이 항상 일치하기 때문에 동작 예측이 쉽고 input값을 여러 군데에서 쉽게 바꿀 수 있음

- 주로 권장되는 방법

 

 

비제어 컴포넌트(Uncontrolled Component)

- input의 value 값을 리액트에서 지정하지 않음

-리액트에서 실제 input값을 제어하지 않음

- 경우에 따라 사용

 

 

5. 입력 폼 정리

 

- 리액트에선 순수 HTML과 다르게 onChange Prop을 사용하면 입력 값이 바뀔 때마다 핸들러 함수를 실행

 

1) 폼을 다루는 기본적인 방법

 

- 스테이트를 만들고 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>
  )
}

 

2) 폼 값을 객체 하나로 처리 

 

- 이벤트 객체의 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>
  )
}

 

3) 기본 submit 동작 막기

 

- HTML 폼의 기본 동작은 submit 타입의 버튼을 눌렀을 때 페이지를 이동하는 것

- 이벤트 객체의 preventDefault를 사용하면 이 동작을 막을 수 있음

const handleSubmit = (e) => {
  e.preventDefault();
  // ...
}

 

 

6. 파일 인풋

- 파일 인풋은 반드시 비제어 input으로 만들어야 함

- 파일 인풋에서는 이벤트 객체의 target,value값이 아니라 target.files를 사용

- handleChange() 라는 함수를 만들고, 첫 번째 파일을 nextValue라는 변수로 지정한 다음 이걸 onChange() 함수로 name과 함께 넘겨주면 됨

 

FileInput.js

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;

 

ReviewForm.js

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

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

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

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    handleChange(name, value);
  };

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

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

export default ReviewForm;

 

7. ref로 DOM 노드 가져오기 

 

- ref prop

 - 원하는 시점에 실제 DOM 노드에 접근하고 싶을 때 사용할 수 있는 prop

 - ref를 쓰면 실제 DOM 노드를 직접 참조 가능

 

FileInput.js

import { useEffect, useRef } from 'react';

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;

 

8. 파일 인풋 초기화

 

- FileInput의 value 속성은 사용자만 직접 바꿀 수 있고 자바스크립트로 바꿀 때는 빈 문자열로만 바꿀 수 있음

- value 속성을 빈 문자열로 바꿔주면 선택한 파일이 초기화 

 

- 파일 인풋 노드를 참조할 Ref 객체 생성( useRef() 함수 사용)

- inputRef 객체를 파일 인풋에 ref Prop으로 내려줌

- handleClearClick 함수에선 인풋 노드에 해당하는 inputRef.current 값이 있는지 확인하고, DOM 노드의 value 값을 빈 문자열로 변경

 

FileInput.js

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;

 

9. ref와 useRef

 

1) Ref 객체 생성

 

 - useRef 함수로 Ref 객체 생성

import { useRef } from 'react';

// ...

const ref = useRef();

 

2) ref Prop 사용

 

- ref Prop에다가 앞에서 만든 Ref 객체를 내려줌

const ref = useRef();

// ...

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

 

3) Ref 객체에서 DOM 노드 참조

 

 - Ref 객체의 current라는 프로퍼티를 사용하면 DOM 노드를 참조할 수 있음

 - current 값은 없을 수도 있으니까 반드시 값이 존재하는지 검사하고 사용해야 함

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

 

9. 이미지 파일 미리보기 

 

- 파일 객체를 Object-URL로 만들면 파일에 대한 주소를 만들 수 있음

- ObjectURL은 URL.createObjectURL이라는 함수로 만들 수 있음

- ObjectURL을 만들면 웹 브라우저는 메모리를 할당하고 파일에 해당하는 주소를 만들어 줌 

- 사이드 이펙트(side effect) : 컴포넌트 함수에서 외부의 상태를 바꿈

- 리액트에서는 사이드 이펙트를 다루는 경우에 주로 useEffect를 사용

 

FileInput.js

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" onChange={handleChange} ref={inputRef} />;
     {value && <button onClick={handleClearClick}>X</button>}
   </div>
 );
}


export default FileInput;

 

 

10. 사이트 이펙트 정리하기

 

- 파일을 선택할 때마다 메모리를 할당하기만 한다면 메모리가 낭비됨

- 다른 파일을 선택하거나 파일 선택을 해제했을 때 메모리도 같이 해제해야함

- revokeObjectURL로 메모리 할당을 해제 

- useEffect의 콜백 함수에서 정리함수를 리턴하면 objectURL을 더이상 사용하지 않을 때 해제 가능

 

FileInput.js

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;

 

 

11. 사이드 이펙트와 useEffect

 

1) 사이드 이펙트

 

- 외부에 부수적인 자굑을 하는 것

- 함수 안에서 함수 바깥에 있는 값이나 상태를 변경하는 것

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 변수)가 바뀌기 때문에, 이런 함수를 "사이드 이펙트가 있다"고 함 

 

 

2) 사이드 이펙트와 useEffect

 

- useEffect는 리액트 컴포넌트 함수 안에서 사이드 이펙트를 실행하고 싶을 때 사용하는 함수

ex) DOM 노드 직접 변경, 브라우저에 데이터를 저장하고 네트워크 리퀘스트를 보내는 것

 

 2-1) 페이지 정보 변경

useEffect(() => {
  document.title = title; // 페이지 데이터를 변경
}, [title]);

 

 2-2) 네트워크 요청

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

 

2-3) 데이터 저장

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

 

2-4) 타이머 

useEffect(() => {
  const timerId = setInterval(() => {
    setSecond((prevSecond) => prevSecond + 1);
  }, 1000); // 1초마다 콜백 함수를 실행하는 타이머 시작
  
  return () => {
    clearInterval(timerId);
  }
}, []);

 

3) useEffect를 쓰면 좋은 경우 

 

 - useEffect는 '동기화'에 쓰면 유용

 - 동기화는 컴포넌트 안에 데이터와 리액트 바깥에 있는 데이터를 일치시키는 것

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;

 - document를 다루는 사이드 이펙트 부분만 따로 처리 

 - setTitle 함수를 쓸 때마다 document.title을 변경하는 코드를 신경쓰지 않아도 됨 

 

 

4) 정리 함수(Cleanup Function)

- useEffect의 콜백 함수에서 사이드 이펙트를 만들면 정리가 필요한 경우가 있음

- 이럴 때 콜백 함수에서 리턴 값으로 정리하는 함수 리턴 가능

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

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

 

4-1) 정리 함수가 실행되는 시점

 

- 콜백을 한 번 실행했으면, 정리 함수도 반드시 한 번 실행됨

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

 

 

12. 별점 컴포넌트, 별점 인풋 만들기 

 

- Star 컴포넌트는 별 하나를 보여주는 컴포넌트 

- Rating 컴포넌트는 별 다섯개를 보여주는 컴포넌트 

 

- 별점을 클릭했을 때는 onSelect 함수가 해당하는 별점 값으로 실행

- 별점에 마우스를 올렸을 때는 onHover 함수가 해당하는 별점값으로 실행

- 마우스가 영역을 벗어나면 onMouseOut을 실행

 

 

Rating.js

import './Rating.css';

const RATINGS = [1, 2, 3, 4, 5];

function Star({ selected = false, rating = 0, onSelect, onHover }) {
  const className = `Rating-star ${selected ? 'selected' : ''}`;

  const handleClick = onSelect ? () => onSelect(rating) : undefined;

  const handleMouesOver = onHover ? () => onHover(rating) : undefined;

  return (
    <span
      className={className}
      onClick={handleClick}
      onMouseOver={handleMouesOver}
    >
      ★
    </span>
  );
}

function Rating({ className, value = 0, onSelect, onHover, onMouseOut }) {
  return (
    <div className={className} onMouseOut={onMouseOut}>
      {RATINGS.map((rating) => (
        <Star
          key={rating}
          selected={value >= rating}
          rating={rating}
          onSelect={onSelect}
          onHover={onHover}
        />
      ))}
    </div>
  );
}

export default Rating;

 

RatingInput.js

import { useState } from 'react';
import Rating from './Rating';
import './RatingInput.css';

function RatingInput ( {name, value, onChange}) {
  const [rating, setRating] = useState(value);

  const handleSelect = (nextValue) => onChange(name, nextValue);

  const handleMouseOut = () => setRating(value);

  return (
    <Rating 
      className = "RatingInput"
      value = {rating}
      onSelect = {handleSelect}
      onHover = {setRating}
      onMouseOut = {handleMouseOut}
     />
  );
}

export default RatingInput;

ReviewForm.js

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

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

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

  const handleInputChange = (e) => {
    const { name, value } = e.target;
    handleChange(name, value);
  };

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

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

export default ReviewForm;