React 데이터 다루기 (2)

깨진알·2023년 12월 28일

React

목록 보기
4/12

데이터 가져오기

1. 리액트에서 fetch 사용하기

// api.js
export async function getReviews() {
  const response = await fetch('https://learn.codeit.kr/api/film-reviews');
  const body = await response.json();
  return body;
}

// App.js
const handleLoadClick = async () => {
  const { reviews } = await getReviews();
  setItems(reviews);
};

2. useEffect로 초기 데이터 가져오기

컴포넌트가 처음 렌더링될 때 request를 보내고 싶을 때 useEffect를 사용하면 된다.

const handleLoad = async () => {
  const { reviews } = await getReviews();
  setItems(reviews);
};

useEffect(() => {
  handleLoad();
}, []);

3. 서버에서 정렬한 데이터 받아오기

// api.js
export async function getReviews(order = 'createdAt') {
  const query = `order=${order}`;
  const response = await fetch(
    `https://learn.codeit.kr/api/film-reviews?${query}`
  );
  const body = await response.json();
  return body;
}

// App.js
useEffect(() => {
  handleLoad(order);
}, [order]);

useEffect는 첫 번째 아규먼트로 콜백 함수를 두 번째 아규먼트로 디펜던시 리스트를 받는다. 리액트에서는 콜백 함수와 디펜던시 리스트를 기억했다가 첫 렌더링될 때 콜백 함수를 실행하고, 이후 렌더링부터는 디펜던시 리스트가 바뀌었을 때만 실행된다.


4. useEffect

(1) 처음 한 번만 실행하기

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

컴포넌트가 처음 렌더링 되고 나면 리액트가 콜백 함수를 기억해뒀다가 실행한다. 그 이후로는 콜백 함수를 실행하지 않는다.

(2) 값이 바뀔 때마다 실행하기

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

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

(3) 실험으로 확인해보기

import { useEffect, useState } from 'react';

function App() {
  const [first, setFirst] = useState(1);
  const [second, setSecond] = useState(1);

  const handleFirstClick = () => setFirst(first + 1);

  const handleSecondClick = () => setSecond(second + 1);

  useEffect(() => {
    console.log('렌더링 이후', first, second);
  }, []);

  console.log('렌더링', first, second);

  return (
    <div>
      <h1>
        {first}, {second}
      </h1>
      <button onClick={handleFirstClick}>First</button>
      <button onClick={handleSecondClick}>Second</button>
    </div>
  );
}

export default App;

디펜던시 리스트에 [], [first], [first, second]를 넣어보면서 콘솔 출력이 어떻게 달라지는지 확인해 보아라.


5. 페이지네이션 (Pagination)

책의 페이지처럼 데이터를 나눠서 제공하는 것을 의미한다. 많은 양의 데이터를 사용할 때 활용한다.

(1) 오프셋(Offset) 기반 페이지네이션

오프셋(Offset)은 상쇄를 의미하며, 지금까지 받아온 데이터의 개수라고 생각하면 된다.


(2) 커서(Cursor) 기반 페이지네이션

커서(Cursor)는 데이터를 가리키는 값으로, 지금까지 받은 데이터를 표시한 책갈피라고 생각하면 된다.


하지만 커서 기반 페이지네이션은 오프셋 기반 페이지네이션 보다 서버 입장에서 만들기 까다롭고, 데이터가 자주 바뀌는 것이 아니라면 오프셋 기반 페이지네이션으로 충분하다.


6. 데이터 더 불러오기

// api.js
export async function getReviews({
  order = 'createdAt',
  offset = 0,
  limit = 6,
}) {
  const query = `order=${order}&offset=${offset}&limit=${limit}`;
  const response = await fetch(
    `https://learn.codeit.kr/api/film-reviews?${query}`
  );
  const body = await response.json();
  return body;
}

// App.js
import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

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

  const handleLoad = async (options) => {
    const { paging, reviews } = await getReviews(options);
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems([...items, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

  return (
    <div>
      <div>
        <button onClick={handleNewestClick}>최신순</button>
        <button onClick={handleBestClick}>베스트순</button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      <button disabled={!hasNext} onClick={handleLoadMore}>
        더 보기
      </button>
    </div>
  );
}

export default App;

7. 데이터가 있을 때만 버튼 보여주기

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

8. 조건부 렌더링 팁

(1) 논리 연산자 활용하기

1. 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이면 렌더링하지 않는다.

2. 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이면 렌더링한다.

(2) 삼항 연산자 활용하기

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의 값이 참일 경우엔 ✅, 거짓일 경우에는 ❎를 렌더링한다.

(3) 렌더링되지 않는 것들

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을 렌더링한다.

(4) 조건부 렌더링을 사용할 때 주의할 점

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;

9. 비동기로 State를 변경할 때 주의할 점

const handleLoad = async (options) => {
  const { paging, reviews } = await getReviews(options);
  if (options.offset === 0) {
    setItems(reviews);
  } else {
    setItems((prevItems) => [...prevItems, ...reviews]);
  }
  setOffset(options.offset + options.limit);
  setHasNext(paging.hasNext);
};

비동기 상황에서 State를 변경할 때, 이전 State값을 사용하려면 setter 함수에서 콜백을 사용해야 한다.


10. useState 뽀개기

(1) 초깃값 지정하기

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

1. 콜백으로 초깃값 지정하기

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

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

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

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

단, 이 콜백 함수가 리턴할 때까지 리액트가 렌더링하지 않고 기다린다는 점에 주의해야 한다. 콜백 함수의 실행이 오래 걸릴수록 초기 렌더링이 늦어진다는 점도 알아두면 좋다.

(2) Setter 함수 사용하기

1. 기본

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

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

배열이나 객체 같은 참조형은 반드시 새로운 값을 만들어서 전달해야 한다.
1-1) 참조형 State 사용의 잘못된 예

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

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

1-2) 참조형 State 사용의 올바른 예

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

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

2. 콜백으로 State 변경

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

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

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

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

11. 네트워크 로딩 처리하기

import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

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

  const handleLoad = async (options) => {
    let result;
    try {
      setIsLoading(true);
      result = await getReviews(options);
    } catch (error) {
      console.error(error);
      return;
    } finally {
      setIsLoading(false);
    }

    const { paging, reviews } = result;
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems((prevItems) => [...prevItems, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

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

export default App;

12. 네트워크 에러 처리하기

import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [loadingError, setLoadingError] = useState(null);
  const [items, setItems] = useState([]);
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

  const handleNewestClick = () => setOrder('createdAt');

  const handleBestClick = () => setOrder('rating');

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

  const handleLoad = async (options) => {
    let result;
    try {
      setLoadingError(null);
      setIsLoading(true);
      result = await getReviews(options);
    } catch (error) {
      setLoadingError(error);
      return;
    } finally {
      setIsLoading(false);
    }

    const { paging, reviews } = result;
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems((prevItems) => [...prevItems, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };

  const handleLoadMore = async () => {
    await handleLoad({ order, offset, limit: LIMIT });
  };

  useEffect(() => {
    handleLoad({ order, offset: 0, limit: LIMIT });
  }, [order]);

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

export default App;
profile
프론트엔드 지식으로 가득찰 때까지

0개의 댓글