리액트로 데이터 다루기 (2) 데이터 가져오기

LeeKyungwon·2024년 4월 11일
0

프론트엔드 

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

fetch 함수 사용하기

async,await

export async function getReviews() {
  const response = await fetch("api 주소");
  const body = await response.json();
  return body;
}

App.js

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

function App() {
  const [order, setOrder] = useState("createdAt");
  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 handleLoadClick = async () => {
    const { reviews } = await getReviews();
    setItems(reviews);
  };

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

export default App;

useEffect

그냥 바로 로드 함수를 생성해서 실행하면 무한 루프에 빠지게 된다.
로드함수를 실행하는 부분을 useEffect를 사용해서 실행하면 맨 처음 렌더링 될 때만 사용되기 때문에 무한 루프를 막을 수 있다.

  const handleLoad = async () => {
    const { reviews } = await getReviews();
    setItems(reviews);
  };
  useEffect(() => {
    handleLoad();
  }, []);

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

useEffect 함수는 맨처음 렌더링이 끝나면 콜백함수를 실행하고, 그 다음에는 디펜던시 리스트를 비교해서 기억했던 값과 다른 경우에만 실행한다.

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

다음과 같은 경우에서 order state가 바뀔 때마다 재렌더링이 일어난다.

useEffect 정리

처음 한 번만 실행하기

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

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

값이 바뀔 때마다 실행하기

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

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

페이지네이션

책의 페이지처럼 데이터를 나눠서 제공하는 것
많은 양의 데이터를 조금씩 나눠서 받아오는 방법

오프셋 기반 페이지네이션

오프셋 (Offset)
= 상쇄하다
= 지금까지 받아온 데이터의 개수를 기준으로 받아온다.

GET https://example.com/posts?offset=20&limit=10

=> 지금까지 20개 받았으니까 10개 더 보내달라는 의미
중간에 데이터가 추가되거나 빠졌을 때, 중복해서 불러오거나 빠지는 문제가 생길 수 있다.

커서 기반 페이지네이션

커서 (Cursor)
= 데이터를 가리키는 커서를 기준으로 받아온다.
= 지금까지 받은 데이터를 표시한 책갈피

GET https://example.com/posts?limit=10

=> 데이터 10개 더 보내달란 의미

리스폰스로 데이터랑 페이지네이션 정보를 보내줄 때, 다음 커서 값도 같이 넘겨준다.
다음 페이지를 불러올 때 아까 받아온 커서값으로 리퀘스트를 보내고 그 커서 이후로 데이터를 요청하게 된다.

GET https://example.com/posts?cursor=WerZxc&limit=10

데이터의 중복이나 빠짐 없이 가져올 수 있다.

서버 입장에서 오프셋 기반보다 만들기 까다롭고 데이터가 자주 바뀌는게 아니라면 오프셋 기반으로도 충분하다.

사용 방법

api주소?offset=0&limit=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;

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

{hasNext && <button onClick={handleLoadMore}>
       더 보기
     </button>
     }

조건부 렌더링

논리 연산자

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;

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;

삼항 연산자

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;

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

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

true나 false 값은 리액트에서 렌더링 하지 않기 때문에 명확한 논리식을 써주는게 안전하다

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;

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

잘못된 시점의 값을 사용하는 문제가 있다.
ex) 더보기 누르고 삭제 눌렀는데 렌더링 되면서 삭제된 값이 되살아난 경우
-> 세터함수의 값이 아니라 콜백을 활용

setItems((prevItems) => [...prevItems, ...reviews]);

prevItems 라는 파라미터로 이전 스테이트 값을 받아서 변경할 스테이트 값을 리턴하게 된다.

UseState 정리

초깃값 지정하기

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

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

콜백으로 초깃값 지정하기

const [state, setState] = useState(() => {
  // 초기값을 계산
  return initialState;
});
function ReviewForm() {
  const [values, setValues] = useState(() => {
    const savedValues = getSavedValues(); // 처음 렌더링할 때만 실행됨
    return savedValues
  });
  // ...
}

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

Setter 함수 사용하기

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

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

배열이나 객체 같은 참조형의 경우 반드시 새로운 값을 만들어서 전달해야 된다.

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

네트워크 로딩 처리하기

isLoading 값을 만들어서 네트워크가 로딩되는 동안 버튼을 비활성화 시킨다.

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;

네트워크 에러 처리하기

 const [loadingError, setLoadingError] = useState(null);
  
 ...
  
 const handleLoad = async (options) => {
    let result;
    try {
      setLoadingError(null);
      setIsLoading(true);
      result = await getReviews(options);
    } catch (error) {
      setLoadingError(error);
      return;
    } finally {
      setIsLoading(false);
    }
    
 ...
{loadingError?.message && <span>{loadingError.message}</span>}
    

에러가 발생할 때 화면에 에러 메세지 출력

검색 기능 추가

api.js

export async function getFoods({
  order = '',
  cursor = '',
  limit = 10,
  search = '',
}) {
  const query = `order=${order}&cursor=${cursor}&limit=${limit}&search=${search}`;
  // ...
}

App.js

import { useEffect, useState } from 'react';
import { getFoods } from '../api';
import FoodList from './FoodList';

function App() {
  const [order, setOrder] = useState('createdAt');
  const [cursor, setCursor] = useState(null);
  const [items, setItems] = useState([]);
  const [isLoading, setIsLoading] = useState(false);
  const [loadingError, setLoadingError] = useState(null);
  const [search, setSearch] = useState('');

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

  const handleCalorieClick = () => setOrder('calorie');

  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 getFoods(options);
    } catch (error) {
      setLoadingError(error);
      return;
    } finally {
      setIsLoading(false);
    }
    const {
      foods,
      paging: { nextCursor },
    } = result;
    if (!options.cursor) {
      setItems(foods);
    } else {
      setItems((prevItems) => [...prevItems, ...foods]);
    }
    setCursor(nextCursor);
  };

  const handleLoadMore = () => {
    handleLoad({
      order,
      cursor,
      search,
    });
  };

  const handleSearchSubmit = (e) => {
    e.preventDefault();
    setSearch(e.target['search'].value);
  };

  const sortedItems = items.sort((a, b) => b[order] - a[order]);

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

  return (
    <div>
      <button onClick={handleNewestClick}>최신순</button>
      <button onClick={handleCalorieClick}>칼로리순</button>
      <form onSubmit={handleSearchSubmit}>
        <input name="search" />
        <button type="submit">검색</button>
      </form>
      <FoodList items={sortedItems} onDelete={handleDelete} />
      {cursor && (
        <button disabled={isLoading} onClick={handleLoadMore}>
          더보기
        </button>
      )}
      {loadingError?.message && <p>{loadingError.message}</p>}
    </div>
  );
}

export default App;

handleLoad, handleLoadMore 함수에 검색어가 바뀔 때마다 새 리퀘스트를 보낼 수 있도록 search를 추가하였고
엔터를 누르거나 검색 버튼을 눌렀을 때 handleSearchSubmit 함수가 실행되도록 하였다.

post-custom-banner

0개의 댓글