본문 바로가기

react

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

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

 

api.js

export async function getReviews() {
  const response = await fetch('https://learn.codeit.kr/6205/film-reviews');
  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;

 

 

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

 

- useEffect 함수에다가 실행할 콜백 함수랑 빈 배열을 넘겨주게 되면 리액트는 콜백 함수를 맨 처음 렌더링할 때만 실행하기 때문에 무한 루프가 생기는 걸 막을 수 있음

- 컴포넌트가 처음 렌더링될 때 리퀘스트를 보내고 싶다면 useEffect 사용

 

App.js

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

 

 

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

 

- useEffect는 인수로 콜백과 디펜던시 리스트를 받음

- useEffect 함수는 맨 처음 렌더링이 끝나면 콜백 함수를 실행해 주고 그 다음부터는 디펜던시 리스트를 비교해서 기억했던 값이랑 다른 경우에만 콜백을 실행

- useEffect를 사용해서 정렬값이 바뀔 때마다 서버에서 데이터를 받아오도록 함

 -> useEffect() 함수의 두 번째 인수로 [order]를 사용하면 order 스테이트값이 바뀔 때마다 콜백 함수 실행

 

api.js

export async function getReviews(order = 'createdAt') {
  const query = `order=${order}`;
  const response = await fetch(
    `https://learn.codeit.kr/6205/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';

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 handleLoad = async (orderQuery) => {
    const { reviews } = await getReviews(orderQuery);
    setItems(reviews);
  };
  
  useEffect(() => {
    handleLoad(order);
  }, [order]);

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

export default App;

 

4. useEffect 정리 (1)

 

1) 처음 한 번만 실행하기 

 

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

 -그 이후로는 콜백함수를 실행하지 않음

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

 

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

 

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

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

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

 

 

5. 페이지네이션

 

페이지네이션(pagination)

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

- 많은 양의 데이터를 제공할 때 사용

- 종류 

 

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

 

 - 개수를 기준으로 데이터를 나눔

 

 오프셋(Offset)

 - 지금까지 받아온 데이터의 개수 

 = 지금까지 20개 받았으니까 10개 더 보내줘라고 요청

 

 

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

 

- 커서를 사용하면 만약에 데이터가 바뀌더라도 커서가 가리키는 데이터는 변하지 않음

- 오프셋과 달리 데이터의 중복이나 빠짐 없이 가져올 수 있음

 

- 커서(Cursor)

 - 특정 데이터를 가리키는 값

 - 지금까지 받은 데이터를 표시한 책갈피

  = 데이터 10개 보내줘

 = 다음 커서 값도 같이 넘겨줌

= 다음 페이지를 불러 올 땐 아까 받아온 커서값으로 리퀘스트를 보냄

= 커서 데이터 이후로 10개 보내줘

 

 

6. 데이터 더 불러오기 


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 });
    // eslint-disable-next-line
  }, [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;

 

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/6205/film-reviews?${query}`
  );
  const body = await response.json();
  return body;
}

 

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

 

- hasNext 값이 참일 때는 뒤에 있는 조건을 계산해서 값을 사용

- 뒤에 있는 표현식이 JSX니까 버튼이 렌더링됨

- hasNext 값이 거짓이면 뒤에 있는 표현식을 계산하지 않고 앞에 조건인 hasNext의 값을 사용하는데 이 값은 false

- 리액트에서 false값은 렌더링하지 않기 때문에 버튼이 보이지 않음

 

App.js

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 onClick={handleLoadMore}>더 보기</button>}
    </div>
  );
}

 

조건부 렌더링

 

1) 논리 연산자 활용

 

AND 연산자 

- show 값이 true이면 렌더링하고, false이면 렌더링하지 않음

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 연산자 

- hide 값이 true이면 렌더링 하지 않고, false이면 렌더링함

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;

 

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;

 

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;

 

- 각각 숫자 0과 1을 렌더링

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

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

export default App;

 

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

 

- 비동기로 State를 변경할 때는 잘못된 시점의 값을 사용하는 문제 발생

- setter 함수에 값이 아니라 콜백을 전달해서 해결

- prevItems라는 파라미터로 이전 State값을 받아서 변경할 State값을 리턴

- prevItems 값은 고정된 게 아니라 함수의 파라미터기 때문에 리액트가 현재 시점의 State 값을 전달해줌

 

App.js

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

 

9. useState 정리(2)

 

1) 초깃값 지정

 

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

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

 

2) 콜백으로 초깃값 지정하기 

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

 

ex)

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

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

 

 

10. Setter 함수 사용

 

1) 기본

 

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

- 참조형은 반드시 새로운 값을 만들어서 전달해야 함

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

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

 

2) 콜백으로 State 변경

 

- 이전 State 값을 참조하면서 State를 변경하는 경우, 비동기 함수에서 State를 변경하게 되면 최신 값이 아닌 State 값을 참조하는 문제 발생 

 -> 콜백을 사용해서 처리 가능 

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

 

11. 네트워크 로딩 처리

 

- 네트워크 상태에 따라서 현재 네트워크가 리퀘스트 중이면 true, 아니면 false 값을 갖는 State 생성

- 네트워크 리퀘스트 전에 값을 true로 변경하고, 네트워크 리퀘스트가 끝나면 값을 false로 변경

- 네트워크 요청이 진행되는 동안에는 더보기 버튼이 누르지 못하도록 비활성화

 

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 [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 });
    // eslint-disable-next-line
  }, [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. 네트워크 에러 처리 

 

- try ... catch에서 에러를 받아 온 다음에 에러 상태를 리액트 State로 관리하면서 보여줄 수 있음

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 });
    // eslint-disable-next-line
  }, [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;