[P3_S2] React로 데이터 다루기

보리·2024년 4월 14일
0

codeit-sprint

목록 보기
17/22

✨배열 렌더링하기

map 으로 렌더링하기

배열 메소드 map에서 콜백 함수의 리턴 값으로 리액트 엘리먼트를 리턴하면 된다.


import items from './pokemons';

function Pokemon({ item }) {
  return (
    <div>
      No.{item.id} {item.name}
    </div>
  );
}

function App() {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Pokemon item={item} />
        </li>
      ))}
    </ul>
  );
}

export default App;

참고로 반드시 JSX의 중괄호 안에서 map 함수를 써야 하는 것은 아님.

아래처럼 renderedItems 라는 변수에 map의 결과를 지정해도 똑같이 렌더링 하게 된다.

import items from './pokemons';

function Pokemon({ item }) {
  return (
    <div>
      No.{item.id} {item.name}
    </div>
  );
}

function App() {
  const renderedItems = items.map((item) => (
    <li key={item.id}>
      <Pokemon item={item} />
    </li>
  ));

  return (
    <ul>
      {renderedItems}
    </ul>
  );
}

export default App;

sort 로 정렬하기

배열 메소드의 sort 메소드를 사용하면 배열을 정렬할 수 있다.

import { useState } from 'react';
import items from './pokemons';

function Pokemon({ item }) {
  return (
    <div>
      No.{item.id} {item.name}
    </div>
  );
}

function App() {
  const [direction, setDirection] = useState(1);
  const handleAscClick = () => setDirection(1);
  const handleDescClick = () => setDirection(-1);

  const sortedItems = items.sort((a, b) => direction * (a.id - b.id));

  return (
    <div>
      <div>
        <button onClick={handleAscClick}>도감번호 순서대로</button>
        <button onClick={handleDescClick}>도감번호 반대로</button>
      </div>
      <ul>
        {sortedItems.map((item) => (
          <li key={item.id}>
            <Pokemon item={item} />
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

filter 로 삭제하기

배열 메소드 중 filter 와 배열형 스테이트를 활용하면 삭제 기능을 간단히 구현할 수 있다.

import { useState } from 'react';
import mockItems from './pokemons';

function Pokemon({ item, onDelete }) {
  const handleDeleteClick = () => onDelete(item.id);

  return (
    <div>
       No.{item.id} {item.name}
      <button onClick={handleDeleteClick}>삭제</button>
    </div>
  );
}

function App() {
  const [items, setItems] = useState(mockItems);

  const handleDelete = (id) => {
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id}>
          <Pokemon item={item} onDelete={handleDelete} />
        </li>
      ))}
    </ul>
  );
}

export default App;

const nextItems = items.filter((item) => [item.id](http://item.id/) !== id);

  • filter 함수는 배열의 각 요소에 대해 주어진 콜백 함수를 실행하고, 그 결과가 참인 요소들로 이루어진 새로운 배열을 반환한다.
  • 예를 들어, items 배열에 다음과 같은 요소들이 있다고 가정하면:
const items = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' },
    { id: 3, name: 'Orange' }
];

그리고 id 변수에는 2가 저장되어 있다고 가정하면

  1. 첫 번째 요소({ id: 1, name: 'Apple' })의 id2와 다르기 때문에 결과에 포함
  2. 두 번째 요소({ id: 2, name: 'Banana' })의 id2와 같기 때문에 결과에 포함되지 않음
  3. 세 번째 요소({ id: 3, name: 'Orange' })의 id2와 다르기 때문에 결과에 포함

반드시 key 를 내려주자

  • 각 요소를 렌더링 할 때는 key Prop을 내려줘야 한다.
  • 이때 가장 바깥쪽에 있는 (최상위) 태그에다가 key Prop을 지정하면 된다.
  • 반드시 id 일 필요는 없고 각 데이터를 구분할 수 있는 고유한 값이면 무엇이든 key 로 활용해도 상관없다.
import items from './pokemons';

function Pokemon({ item }) {
  return (
    <div>
      No.{item.id} {item.name}
    </div>
  );
}

function App() {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.name}>
          <Pokemon item={item} />
        </li>
      ))}
    </ul>
  );
}

export default App;

❓배열을 렌더링할 때 key를 지정하지 않으면?

  • key를 지정하더라도 배열의 인덱스같이 데이터를 구분할 수 없는 고유하지 않은 값으로 key를 지정하면 렌더링이 잘못될 수 있다.
  • 각 데이터를 구분할 수 있는 고유한 값으로 key를 꼭 정해줘야 함!!

데이터 가져오면서 무한루프에 빠짐

import React, { useState } from "react";
import ReviewList from "./components/ReviewList";
import { getReviews } from "./api";

function App() {
  const [items, setItems] = useState([]);
  const [order, setOrder] = useState("createdAt"); // 최신순
  const sortedItems = items.sort((a, b) => b[order] - a[order]); // 평점 높은 순(내림차순)

  const handleNewestClick = () => setOrder("createdAt");
  const handBestClick = () => setOrder("rating");
  const handleDelete = (id) => {
    // 현재 순회 중인 요소의 id가 주어진 id와 같지 않은 경우를 체크
    const nextItems = items.filter((item) => item.id !== id);
    setItems(nextItems);
  };

  const handleLoad = async () => {
    // 비동기로 리퀘스를 보냈다가 리스폰스가 도착하면 reviews 변수를 지정하고
    const { reviews } = await getReviews();
    // setItems를 통해 state 변경 -> App 컴포넌트를 다시 렌더링함
    setItems(reviews)
  };

  // 다시 이 함수가 렌더링 되어 무한루프가 생김
  handleLoad()

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      <button>불러오기</button>
    </div>
  );
}

export default App;

→ useEffect로 해결하기

✨useEffect

📘처음 한 번만 실행하기

useEffect(() => {
  // 실행할 코드
}, []);

컴포넌트가 처음 렌더링 되고 나면 리액트가 콜백 함수를 기억해뒀다가 실행한다.

그 이후로는 콜백 함수를 실행하지 않는다.

📘값이 바뀔 때마다 실행하기

useEffect(() => {
  // 실행할 코드
}, [dep1, dep2, dep3, ...]);

컴포넌트가 처음 렌더링 되고 나면 리액트가 콜백 함수를 기억해뒀다가 실행한다.

그 이후로 렌더링 할 때는 디펜던시 리스트에 있는 값들을 확인해서 하나라도 바뀌면 콜백 함수를 기억해뒀다가 실행한다.

✨페이지네이션

책의 페이지처럼 데이터를 나눠서 제공하는 것

📘오프셋 기반 페이지네이션

offset: 상쇄하다 = 지금까지 받아온 데이터의 개수

  • 받아온 개수 기준

📘커서 기반 페이지네이션

cursor: 데이터를 가리키는 값 = 지금까지 받은 데이터를 표시한 책갈피

데이터가 바뀌더라고 커서가 가리키는 데이터 값은 변하지 않음 → 데이터 중복이나 빠짐없이 데이터를 잘 가져온다.

  • 데이터를 가리키는 커서 기준

✨조건부 렌더링

논리 연산자 활용하기

📘AND 연산자


import { useState } from 'react';

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

  const handleClick = () => setShow(!show);

  return (
    <div>
      <button onClick={handleClick}>토글</button>
      {show && <p>보인다 👀</p>}
    </div>
  );
}

export default App;

show 값이 true 이면 렌더링 하고, false 이면 렌더링 하지 않는다.

OR 연산자


import { useState } from 'react';

function App() {
  const [hide, setHide] = useState(true);

  const handleClick = () => setHide(!hide);

  return (
    <div>
      <button onClick={handleClick}>토글</button>
      {hide || <p>보인다 👀</p>}
    </div>
  );
}

export default App;

hide 값이 true 이면 렌더링 하지 않고, false 이면 렌더링 한다.

📘삼항 연산자 활용하기


import { useState } from 'react';

function App() {
  const [toggle, setToggle] = useState(false);

  const handleClick = () => setToggle(!toggle);

  return (
    <div>
      <button onClick={handleClick}>토글</button>
      {toggle ? <p></p> : <p></p>}
    </div>
  );
}

export default App;

삼항 연산자를 사용하면 참, 거짓일 경우에 다르게 렌더링해줄 수 있다.

toggle 의 값이 참일 경우엔 '✅'을, 거짓일 경우에는 '❎'를 렌더링한다.

📘렌더링되지 않는 값들


function App() {
  const nullValue = null;
  const undefinedValue = undefined;
  const trueValue = true;
  const falseValue = false;
  const emptyString = '';
  const emptyArray = [];

  return (
    <div>
      <p>{nullValue}</p>
      <p>{undefinedValue}</p>
      <p>{trueValue}</p>
      <p>{falseValue}</p>
      <p>{emptyString}</p>
      <p>{emptyArray}</p>
    </div>
  );
}

export default App;

위 컴포넌트에서 중괄호 안에 있는 값들은 모두 아무것도 렌더링하지 않는다.


function App() {
  const zero = 0;
  const one = 1;

  return (
    <div>
      <p>{zero}</p>
      <p>{one}</p>
    </div>
  );
}

export default App;

이 값들은 각각 숫자 0과 1을 렌더링 한다.

⚠️조건부 렌더링을 사용할 때 주의할 점


import { useState } from 'react';

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

  const handleClick = () => setNum(num + 1);

  return (
    <div>
      <button onClick={handleClick}>더하기</button>
      {num && <p>num이 0 보다 크다!</p>}
    </div>
  );
}

export default App;

num 값이 0일 때는 false 로 계산되니까 뒤의 값을 계산하지 않기 때문에 아무것도 렌더링 하지 않는 코드 같지만, 숫자 0은 0으로 렌더링 된다.

그래서 처음 실행했을 때 숫자 0이 렌더링 되고 '더하기' 버튼을 눌러서 num 값이 증가하면 num이 0 보다 크다! 가 렌더링 된다.

그래서 이런 경우엔 아래처럼 보다 명확한 논리식을 써주는 게 좋다.

truefalse 값은 리액트에서 렌더링 하지 않기 때문!!


import { useState } from 'react';

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

  const handleClick = () => setNum(num + 1);

  return (
    <div>
      <button onClick={handleClick}>더하기</button>
      {(num > 0) && <p>num이 0 보다 크다!</p>}
    </div>
  );
}

export default App;

✨useState

📘초깃값 지정하기

const [state, setState] = useState(initialState);

useState 함수에 값을 전달하면 초깃값으로 지정할 수 있다.

📘콜백으로 초깃값 지정하기

const [state, setState] = useState(() => {
  // 초기값을 계산
  return initialState;
});

초깃값을 계산해서 넣는 경우 콜백을 사용하면 좋다.

function ReviewForm() {
  const savedValues = getSavedValues(); // ReviewForm을 렌더링할 때마다 실행됨
  const [values, setValues] = useState(savedValues);
  // ...
}

getSavedValues 라는 함수를 통해서 컴퓨터에 저장된 초깃값을 가져온다고 해보면,

savedValues 라는 값은 처음 렌더링 한 번만 계산하면 되는데, 매 렌더링 때마다 불필요하게 getSavedValues 함수를 실행해서 저장된 값을 가져온다.


function ReviewForm() {
  const [values, setValues] = useState(() => {
    const savedValues = getSavedValues(); // 처음 렌더링할 때만 실행됨
    return savedValues
  });
  // ...
}

이럴 때는 이렇게 콜백 형태로 초깃값을 지정해주면 처음 렌더링 할 때 한 번만 콜백을 실행해서 초깃값을 만들고, 그 이후로는 콜백을 실행하지 않기 때문에 getSavedValues 를 불필요하게 실행하지 않는다.

단, 이때 이 콜백 함수가 리턴할 때까지 리액트가 렌더링하지 않고 기다린다.
콜백 함수의 실행이 오래 걸릴 수록 초기 렌더링이 늦어진다.

✨Setter 함수 사용하기

const [state, setState] = useState(0);

const handleAddClick = () => {
  setState(state + 1);
}

Setter 함수에다가 값을 전달하면, 해당하는 값으로 변경된다.

주의할 점) 배열이나 객체 같은 참조형은 반드시 새로운 값을 만들어서 전달해야 한다.

참조형 State 사용의 잘못된 예


const [state, setState] = useState({ count: 0 });

const handleAddClick = () => {
  state.count += 1; // 참조형 변수의 프로퍼티를 수정
  setState(state); // 참조형이기 때문에 변수의 값(레퍼런스)는 변하지 않음
}

참조형 State 사용의 올바른 예


const [state, setState] = useState({ count: 0 });

const handleAddClick = () => {
  setState({ ...state, count: state.count + 1 }); // 새로운 객체 생성
}

📘콜백으로 State 변경


setState((prevState) => {
  // 다음 State 값을 계산
  return nextState;
});

만약 이전 State 값을 참조하면서 State를 변경하는 경우, 비동기 함수에서 State를 변경하게 되면 최신 값이 아닌 State 값을 참조하는 문제가 있다.
이럴 때는 콜백을 사용해서 처리할 수 있다. 파라미터로 올바른 State 값을 가져와서 사용할 수 있다.

콜백으로 State를 변경하는 예시


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

const handleAddClick = async () => {
  await addCount();
  setCount((prevCount) => prevCount + 1);
}

✨입력 폼 (form)

📘onChange

  • 리액트에선 순수 HTML과 다르게 onChange Prop을 사용하면 입력 값이 바뀔 때마다 핸들러 함수를 실행한다.
  • oninput 이벤트와 비슷.

📘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.nametarget.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();
  // ...
}

✨제어 컴포넌트(Controlled Component)

  • 인풋의 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>
  )
}

✨비제어 컴포넌트(Unontrolled Component)

  • 인풋의 value값을 리액트에서 지정하지 않음
  • file input
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 함수에서는 폼 태그를 참조할 수 있다.
👇🏻값들을 참조하려면 이벤트 객체의 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);
  // ...
}

✨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 값은 없을 수도 있으니까 반드시 값이 존재하는지 검사하고 사용해야 하는 점!!!

예시: 이미지 크기 구하기

img 노드의 크기를 ref 를 활용해서 출력하는 예시

img 노드에는 너비 값인 width 와 높이 값인 height 라는 속성이 있다.

Ref 객체의 current 로 DOM 노드를 참조해서 두 속성 값을 가져왔다.


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>
  );
}

✨사이드 이펙트(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]);

참고: localStorage 는 웹 브라우저에서 데이터를 저장할 수 있는 기능

📘타이머


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

  return () => {
    clearInterval(timerId);
  }
}, []);

참고: setInterval 이라는 함수를 쓰면 일정한 시간마다 콜백 함수를 실행할 수 있다.

✨useEffect를 쓰면 좋은 경우

데이터 불러오는 기능 만들 때 처음에는 onclick 이벤트 핸들러에서 네트워크 리퀘스트를 보냈고, 페이지가 열리자마자 데이터를 불러오기 위해서 useEffect 를 사용하는 걸로 바꿨다.

하지만 만약 둘 다 가능한 경우라면 핸들러 함수를 써야 하는 걸까? 아니면 useEffect 를 써야 하는 걸까?

정해진 규칙은 없습니다. 하지만 useEffect 는 쉽게 말해서 '동기화'에 쓰면 유용한 경우가 많다. (동기화는 컴포넌트 안에 데이터와 리액트 바깥에 있는 데이터를 일치시키는 것)

📍핸들러 함수만 사용한 예시

// 인풋 입력에 따라 페이지 제목을 바꾸는 컴포넌트
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 값도 변경해야 한다는 걸 기억해뒀다가 관련된 코드를 작성해야 한다는 점

📍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;
  • useEffect 를 사용한 예시에서는 document를 다루는 사이드 이펙트 부분만 따로 처리하고 있다.
  • 이렇게 하면 setTitle 함수를 쓸 때마다 document.title 을 변경하는 코드를 신경 쓰지 않아도 되니까 편리하다.
  • 게다가 처음 렌더링 되었을 때 'Untitled'라고 페이지 제목을 변경하는 효과까지 낼 수 있다.
  • useEffect 는 리액트 안과 밖의 데이터를 일치시키는데 활용하면 좋다. (useEffect 를 사용했을 때 반복되는 코드를 줄이고, 동작을 쉽게 예측할 수 있는 코드를 작성할 수 있기 때문)

✨정리 함수 (Cleanup Function)

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

  return () => {
    // 사이드 이펙트에 대한 정리
  }
}, [dep1, dep2, dep3, ...]);
  • useEffect 의 콜백 함수에서 사이드 이펙트를 만들면 정리가 필요한 경우가 있다.
  • 이럴 때 콜백 함수에서 리턴 값으로 정리하는 함수를 리턴할 수 있다.
  • 리턴한 정리 함수에서는 사이드 이펙트에 대한 뒷정리를 한다.

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

📘정리 함수가 실행되는 시점

쉽게 말해서 콜백을 한 번 실행했으면, 정리 함수도 반드시 한 번 실행된다.

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

📍예시: 타이머


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 컴포넌트를 렌더링 하지 않는다.
    • 그럼 리액트에선 마지막으로 앞에서 기억해뒀던 정리 함수를 실행해준다.
    • 타이머를 멈추고 콘솔에는 '타이머 멈춤 ✋'이 출력된다.

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

✨리액트 훅

리액트가 제공하는 기능에 연결해서 그 값이나 기능을 사용하는 함수

📍Hook의 규칙

  • 반드시 리액트 컴포넌트 함수(Functional Component) 안에서 사용해야 함
  • 컴포넌트 함수의 최상위에서만 사용 (조건문, 반복문 안에서 못 씀)

📘useState

State 사용하기

const [state, setState] = useState(initialState);

콜백으로 초깃값 지정하기

초깃값을 계산하는 코드가 복잡한 경우에 활용

const [state, setState] = useState(() => {
  // ...
  return initialState;
});

State 변경

setState(nextState);

이전 State를 참조해서 State 변경

비동기 함수에서 최신 State 값을 가져와서 새로운 State 값을 만들 때

setState((prevState) => {
  // ...
  return nextState
});

📘useEffect

컴포넌트 함수에서 사이드 이펙트(리액트 외부의 값이나 상태를 변경할 때)에 활용하는 함수

처음 렌더링 후에 한 번만 실행

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

렌더링 후에 특정 값이 바뀌었으면 실행

  • 참고로 처음 렌더링 후에도 한 번 실행됨
useEffect(() => {
  // ...
}, [dep1, dep2, dep3, ...]);

사이드 이펙트 정리(Cleanup)하기

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

  return () => {
    // 정리
  }
}, [dep1, dep2, dep3, ...]);

📘useRef

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

const ref = useRef();

// ...

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

DOM 노드 참조하기

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

📘useCallback

함수를 매번 새로 생성하는 것이 아니라 디펜던시 리스트가 변경될 때만 함수를 생성

const handleLoad = useCallback((option) => {
  // ...
}, [dep1, dep2, dep3, ...]);

📘Custom Hook

자주 사용하는 Hook 코드들을 모아서 함수로 만들 수 있다.

이때 useOOO 처럼 반드시 맨 앞에 use 라는 단어를 붙여서 다른 개발자들이 Hook이라는 걸 알 수 있게 해줘야 한다.

useHooks – The React Hooks Library

https://github.com/streamich/react-use

📗useAsync

비동기 함수의 로딩, 에러 처리를 하는 데 사용할 수 있는 함수

함수를 asyncFunction 이라는 파라미터로 추상화해서 wrappedFunction 이라는 함수를 만들어 사용하는 방식을 눈여겨보자.


function useAsync(asyncFunction) {
  const [pending, setPending] = useState(false);
  const [error, setError] = useState(null);

  const wrappedFunction = useCallback(async (...args) => {
    setPending(true);
    setError(null);
    try {
      return await asyncFunction(...args);
    } catch (error) {
      setError(error);
    } finally {
      setPending(false);
    }
  }, [asyncFunction]);

  return [pending, error, wrappedFunction];
}

📗useToggle

toggle 함수를 호출할 때마다 value 값이 참/거짓으로 번갈아가며 바뀐다.

ON/OFF 스위치 같은 걸 만들 때 유용함


function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue((prevValue) => !prevValue);
  return [value, toggle];
}

📗useTimer

start 를 실행하면 callback 이라는 파라미터로 넘겨 준 함수를 timeout 밀리초 마다 실행하고, stop 을 실행하면 멈춘다.

setInterval 이란 함수는 웹 브라우저에 함수를 등록해서 일정한 시간 간격마다 실행한다. 실행할 때마다 사이드 이펙트를 만들고, 사용하지 않으면 정리를 해줘야 한다.

clearInterval 이라는 함수를 실행해서 사이드 이펙트를 정리하는 부분을 눈여겨보자.


function useTimer(callback, timeout) {
  const [isRunning, setIsRunning] = useState(false);

  const start = () => setIsRunning(true);

  const stop = () => setIsRunning(false);

  useEffect(() => {
    if (!isRunning) return;

    const timerId = setInterval(callback, timeout); // 사이드 이펙트 발생
    return () => {
      clearInterval(timerId); // 사이드 이펙트 정리
    };
  }, [isRunning, callback, timeout]);

  return [start, stop];
}

✨디펜던시(exhaustive-deps)

📘exhaustive-deps 규칙

아래 코드는 num 버튼을 누르면 num 스테이트 값이 증가되고,

count 버튼을 누르면 count 스테이트 값을 증가시키는 컴포넌트다.

이때 count 스테이트 값을 증가시키면서 콘솔에는 num 스테이트 값을 출력한다.

useEffect Hook에서는 1초마다 addCount 함수를 실행하는 타이머를 실행한다.


import { useEffect, useState } from 'react';

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  const addCount = () => {
    setCount(c => c + 1);
    console.log(`num: ${num}`);
  }

  const addNum = () => setNum(n => n + 1);

  useEffect(() => {
    console.log('timer start');
    const timerId = setInterval(() => {
      addCount();
    }, 1000);

    return () => {
      clearInterval(timerId);
      console.log('timer end');
    };
  }, []);

  return (
    <div>
      <button onClick={addCount}>count: {count}</button>
      <button onClick={addNum}>num: {num}</button>
    </div>
  );
}

export default App;

이 코드를 실행해보면 1초마다 count 값이 증가하는데,

버튼을 클릭해서 num 스테이트의 값이 바뀌더라도 콘솔 출력에서는 숫자가 바뀌지 않고 0만 계속 출력된다는 문제가 있다.

그 이유는 useEffect 안에서 addCount 라는 함수를 사용하는데, 이 함수에서는 num 스테이트 값을 잘못 참조하기 때문이다. (과거의 num 스테이트 값을 계속해서 참조하고 있기 때문)

⚠️이런 문제점을 경고해주는 규칙이 react-hooks/exhaustive-deps 라는 규칙

리액트에서는 Prop이나 State와 관련된 값은 되도록이면 빠짐없이 디펜던시에 추가해서 항상 최신 값으로 useEffectuseCallback 을 사용하도록 권장하고 있다.


import { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  const addCount = () => {
    setCount((c) => c + 1);
    console.log(`num: ${num}`);
  };

  const addNum = () => setNum((n) => n + 1);

  useEffect(() => {
    console.log('timer start');
    const timerId = setInterval(() => {
      addCount();
    }, 1000);

    return () => {
      clearInterval(timerId);
      console.log('timer end');
    };
  }, [addCount]);

  return (
    <div>
      <button onClick={addCount}>count: {count}</button>
      <button onClick={addNum}>num: {num}</button>
    </div>
  );
}

export default App;

하지만 이렇게 하면 한 가지 문제가 있다.

실행해서 콘솔을 확인해보면 count 가 바뀔 때마다 타이머를 새로 시작하고 종료하는 걸 반복한다.

addCount 라는 함수는 렌더링 할 때마다 새로 만들어지는데, 이걸 디펜던시 리스트에 추가했기 때문에 useEffect 의 콜백이 매번 불필요하게 실행되는 버그가 있다.

📘useCallback으로 함수 재사용하기


import { useCallback, useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  const addCount = useCallback(() => {
    setCount((c) => c + 1);
    console.log(`num: ${num}`);
  }, [num]);

  const addNum = () => setNum((n) => n + 1);

  useEffect(() => {
    console.log('timer start');
    const timerId = setInterval(() => {
      addCount();
    }, 1000);

    return () => {
      clearInterval(timerId);
      console.log('timer end');
    };
  }, [addCount]);

  return (
    <div>
      <button onClick={addCount}>count: {count}</button>
      <button onClick={addNum}>num: {num}</button>
    </div>
  );
}

export default App;

디펜던시 리스트에 추가한 함수가 매번 바뀌는 문제를 해결하려면 함수를 useCallback 으로 감싸주면 된다.

useCallback 을 사용하면 함수를 매번 생성하는 게 아니라 리액트에다 함수를 기억해둘 수 있다.

이때 리액트는 useCallback 의 디펜던시 리스트 값이 바뀔 때만 함수를 새로 만들어준다. addCount 함수에서는 num 이라는 스테이트를 참조하고 있으니까 이 값을 디펜던시 리스트에 추가했다. 이렇게하면 리액트는 num 값이 바뀔 때만 addCount 함수를 새로 만들 것이다.

useEffect 의 디펜던시로 addCount 가 들어가 있으니까, 결론적으로 useEffect 의 콜백은 num 값이 바뀔 때만 새로 실행된다.

이런 식으로 컴포넌트 안에서 만든 함수를 디펜던시 리스트에 사용할 때는 useCallback 훅으로 매번 함수를 새로 생성하는 걸 막을 수 있다.

📘파라미터를 활용하자

addCount 라는 함수에서 num 값을 꼭 직접 참조할 필요는 없다.

그래서 useCallback 을 쓰지 않고, 아래처럼 파라미터로 받아오게 할 수 있다.

이렇게 하면 addCount 함수 자체만 놓고 보면 바깥에 있는 스테이트 값을 직접적으로 참조하지 않기 때문에 오래된 스테이트 값을 참조할 염려가 없다.


import { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);
  const [num, setNum] = useState(0);

  const addCount = (log) => {
    setCount((c) => c + 1);
    console.log(log);
  }

  const addNum = () => setNum((n) => n + 1);

  useEffect(() => {
    console.log('timer start');
    const timerId = setInterval(() => {
      addCount(`num ${num}`);
    }, 1000);

    return () => {
      clearInterval(timerId);
      console.log('timer end');
    };
  }, [num]);

  return (
    <div>
      <button onClick={addCount}>count: {count}</button>
      <button onClick={addNum}>num: {num}</button>
    </div>
  );
}

export default App;

디펜던시 리스트를 이렇게 바꾸면 num 값이 바뀔 때마다 타이머를 재시작한다는 게 좀 더 명확해진다.

Prop이나 State 값을 사용할 때는 이렇게 되도록이면 파라미터로 넘겨서 사용하면,

어떻게 사용되는지 코드에서 명확하게 보여줄 수 있다.

✨Context

  • 많은 컴포넌트에서 사용하는 데이터를 반복적인 prop(prop drilling) 전달 없이 공유

prop Drilling

상위 컴포넌트에서 하위 컴포넌트로 반복해서 prop을 내려주는 상황

→ 해결) context를 만듦

📘Context 만들기

Context는 createContext 라는 함수를 통해 만들 수 있다.

import { createContext } from 'react';

const LocaleContext = createContext();

참고로 이때 아래처럼 기본값을 넣어줄 수도 있다.


import { createContext } from 'react';

const LocaleContext = createContext('ko');

📘Context 적용하기

Context를 쓸 때는 반드시 값을 공유할 범위를 정하고 써야 한다.

이때 범위는 Context 객체에 있는 Provider 라는 컴포넌트로 정해줄 수 있다.

이때 Providervalue prop으로 공유할 값을 내려주면 된다.


import { createContext } from 'react';

const LocaleContext = createContext('ko');

function App() {
  return (
    <div>
       ... 바깥의 컴포넌트에서는 LocaleContext 사용불가

       <LocaleContext.Provider value="en">
          ... Provider 안의 컴포넌트에서는 LocaleContext 사용가능
       </LocaleContext.Provider>
    </div>
  );
}

📘Context 값 사용하기

useContext 라는 Hook을 사용하면 값을 가져와 사용할 수 있다.

이때 아규먼트로는 사용할 Context를 넘겨주면 된다.


import { createContext, useContext } from 'react';

const LocaleContext = createContext('ko');

function Board() {
  const locale = useContext(LocaleContext);
  return <div>언어: {locale}</div>;
}

function App() {
  return (
    <div>
       <LocaleContext.Provider value="en">
          <Board />
       </LocaleContext.Provider>
    </div>
  );
}

📘State, Hook와 함께 활용하기

Provider 역할을 하는 컴포넌트를 하나 만들고, 여기서 State를 만들어서 value 로 넘겨줄 수 있다.

그리고 아래의 useLocale 같이 useContext 를 사용해서 값을 가져오는 커스텀 Hook을 만들 수도 있다. 이렇게 하면 Context에서 사용하는 State 값은 반드시 우리가 만든 함수를 통해서만 쓸 수 있기 때문에 안전한 코드를 작성하는데 도움이 된다.


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

const LocaleContext = createContext({});

export function LocaleProvider({ children }) {
  const [locale, setLocale] = useState();
  return (
    <LocaleContext.Provider value={{ locale, setLocale }}>
      {children}
    </LocaleContext.Provider>
  );
}

export function useLocale() {
  const context = useContext(LocaleContext);

  if (!context) {
    throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
  }

  const { locale } = context;
  return locale;
}

export function useSetLocale() {
  const context = useContext(LocaleContext);

  if (!context) {
    throw new Error('반드시 LocaleProvider 안에서 사용해야 합니다');
  }

  const { setLocale } = context;
  return setLocale;
}
profile
정신차려 이 각박한 세상속에서

0개의 댓글