본문 바로가기

React

React - 데이터 다루기

1. mock data 사용해보기

  • mock은 흉내낸다는 의미라고함.
  • 데이터를 흉내낸 가짜 데이터라고 보면되겠다.

map으로 데이터 렌더링하기

컴포넌트의 return문안에서 map메서드를 사용하여 jsx를 리턴하면, jsx를 여러개 추가한 것 처럼 동작한다.

function ReviewList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li>
          <ReviewListItem item={item} />
        </li>
      ))}
    </ul>
  );
}

위와 같이 컴포넌트가 return하는 jsx안에 {}중괄호를 넣어서 items.map을 실행한다.

      {items.map((item) => (
        <li key={item.id}>
          <ReviewListItem item={item} />
        </li>
      ))}

위 코드는 원래 아래와 같이 작성하려했는데, esLint오류라고 난리쳐서 바꿨다.

esLint에 의하면, 객체를 반환하는경우(???) arrowFunction을 사용할 경우 중괄호와 return키워드를 사용하지 말란다.

{items.map((item) => {
    return (
      <li>
        <ReviewListItem item={item} />
      </li>
    )})}

map메서드의 key

  • map메서드 사용시에는, map내부의 (아마도)최상위 노드에 key property를 꼭 넣어줘야한다.
  • key는 데이터를 언제는지 식별할 수 있는 고유한 값을 사용해줘야한다.
  • 그렇기때문에 map메서드가 파라미터로 받는 index같은 값은 key로 사용하면 안된다.
  • data를 식별할 수 있는 id값 등이 적당하겠다.

데이터 정렬하기

배열의 sort메서드를 사용할 것이다.

import React, { useState } from 'react';
import ReviewList from './ReviewList';
import items from '../mock.json';

function App() {
  const [order, setOrder] = useState('createdAt');
  const sortedItems = items.sort((a, b) => b[order] - a[order]);

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

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

export default App;
  • 위의 코드에서는 정렬 기준이 되는 컬럼을 담을 order state를 만들었다.
  • 초기값으로는 createdAt을 주었기때문에, 맨 처음 화면이 렌더링되면 등록날짜순으로 정렬이 될 것이다.
  • sortedItems 변수에는 item을 sort한 배열이 들어있는데, b[order] - a[order] 를 리턴하고있으니 등록날짜 최신순이겠다.
  • 맨 밑에 <ReviewList items={sortedItems} /> 이 부분에서는, 그냥 tiems가 아닌, sortedItems를 넘김으로써 정렬이 완료된 데이터들이 화면에 렌더링 될 것이다.
  • handleNewestClick 함수와 handleBestClick 함수를 버튼의 onClick으로 주어서 order state를 set하고있다.

데이터 삭제하기(삭제한것처럼보이게하기)

filter메서드를 사용할 것이다.

import React, { useState } from 'react';
import ReviewList from './ReviewList';
import mockItems from '../mock.json';

function App() {
  const [items, setItems] = useState(mockItems);
  const [order, setOrder] = useState('createdAt');
  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);
  };

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

export default App;
import React from 'react';
import './ReviewList.css';

function formatDate(value) {
  const date = new Date(value);
  return `${date.getFullYear()}. ${date.getMonth() + 1}. ${date.getDate()}`;
}

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

  return (
    <div className="ReviewListItem">
      <img className="ReviewListItem-img" src={item.imgUrl} alt={item.title} />
      <div>
        <h1>{item.title}</h1>
        <p>{item.rating}</p>
        <p>{formatDate(item.createdAt)}</p>
        <p>{item.content}</p>
        <button type="button" onClick={handleDeleteClick}>
          삭제
        </button>
      </div>
    </div>
  );
}

function ReviewList({ items, onDelete }) {
  return (
    <ul>
      {items.map((item) => (
        <li>
          <ReviewListItem item={item} onDelete={onDelete} />
        </li>
      ))}
    </ul>
  );
}
export default ReviewList;
  • handleDelete함수를 만들었다.
  • 이 함수는 id를 파라미터로 받아서 items안에 해당 아이디를 제외한 배열을 nextItems에 담고, nextItems를 setItems의 파라미터로 넣었다.
  • 따라서 선택된 id를 뺀 나머지 배열이 렌더링 될테니 결과적으로 선택된 id만 제외된 것이다.
  • 그리고 ReviewList 컴포넌트에 prop으로 onDelete를 보냈고,
  • ReviewList에서는 ReviewListItem컴포넌트로 onDelete prop을 보냈다.
  • ReviewListItems에서는 삭제버튼을 클릭하면 handleDeleteClick을 실행시킨다.
  • handleDeleteClick함수는 prop으로 받은 onDelete함수에 item의 아규먼트를 넣어 실행시킨다.
  • 결과적으로 위 코드는 handleDelete함수에 item의 id를 아규먼트로 넣어 실행시키는 것이다.

2. 백엔드 서버와 연결하기

// api.js

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

import React, { useState } from 'react';
import ReviewList from './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 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 type="button" onClick={handleNewestClick}>
          최신순
        </button>
        <button type="button" onClick={handleBestClick}>
          베스트순
        </button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
      <button type="button" onClick={handleLoadClick}>
        불러오기
      </button>
    </div>
  );
}

export default App;
  • 불러오기 버튼을 누르면, handleLoadClick함수가 실행되고, getReviews를 통해 서버에 request를 보낸다.
  • 받아온 response는 setItems의 파라미터로 넣는다.

 

만약 버튼을 누르지 않고, 페이지가 로드되자마자 데이터를 받아오려면?

useEffect를 사용할것이다.

import React, { useEffect, useState } from 'react';
import ReviewList from './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 handleBestClick = () => setOrder('rating');

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

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

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

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

export default App;
  • useEffect 부분을 보면, useEffect에 콜백함수와 빈 배열을 넘겨주고 있다.
  • 빈 배열을 넘겨주면, 같이 넘겨진 콜백함수가 맨 처음 렌더링 할 때 단 1번만 실행된다.
  • 처음 렌더링 될 때 1회만 request를 보내고 싶다면, useEffect의 두번째 인자로 빈배열을 넣어주면 된다.
  • useEffect의 두번째 인자를 dependency list 라고 부른다.

 

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

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

useEffect함수는 콜백함수와 배열을 파라미터로 받는다.

콜백함수는 react가 비동기로 실행할 함수이고, 배열은 dependency list라고 부르는 값이다.

useEffect를 실행하면, 콜백함수를 예약해뒀다가 렌더링 이후에 실행하는데 이 때 디펜던시 리스트도 기억해뒀다가 이후 다시 실행되었을 때 이 디펜던시 리스트의 값이 달라졌는지를 확인한다.

위의 예시(백엔드 서버와 연결하기 부분의 예제)에서는 아래와같은 순서로 실행된다.

 

  1. 코드를 읽어내리다가 useEffect를 만나 콜백함수를 예약한다.
  2. 렌더링이 끝나면 콜백함수를 실행한다. 이 때 디펜던시 리스트의 값도 기억해둔다.
  3. 콜백함수에서 state의 값이 바뀌고 있으니, app.js가 다시 실행된다.
  4. 다시 useEffect를 만났지만, 현재의 디펜던시리스트와 과거의 디펜던시 리스트를 비교해보니 똑같다.(빈 배열로 똑같다.) 따라서 이번엔 콜백을 예약하지 않는다.
  5. 렌더링이 마무리된다.
  6. 콜백함수가 예약되지 않았기 때문에 handleLoad함수가 재실행되지 않는다.
useEffect(() => {
  handleLoad();
}, [order]);

위와같이, useEffect함수의 두번째 파라미터(디펜던시 리스트)에 order 를 넣으면, order state가 변경될 때 마다 handleLoad함수가 실행되어 서버에 새 데이터를 요청하게 된다.

 

이제 정렬된 데이터를 요청해서 받아와보자.

// api.js

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

import React, { useEffect, useState } from 'react';
import ReviewList from './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 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 type="button" onClick={handleNewestClick}>
          최신순
        </button>
        <button type="button" onClick={handleBestClick}>
          베스트순
        </button>
      </div>
      <ReviewList items={sortedItems} onDelete={handleDelete} />
    </div>
  );
}

export default App;
  • 정렬된 데이터를 받아오는 순서는 아래와 같다.
  • 먼저 useEffect함수에서는 handleLoad함수를 실행할 때, order state값을 인자로 넣어준다.
  • handleLoad함수는 받아온 order state값을 orderQuery라는 이름으로 받는다.
  • 파라미터로 받은 orderQuery는 geReviews함수에 전달된다.
  • getRevies함수에는 order라는 변수로 해당 쿼리를 받아와서 url의 쿼리스트링 문법에 맞게 조작 후, url의 뒤에 붙여주었다.
  • 그럼 서버보내는 request에 order에 대한 쿼리스트링이 함께 보내질 것이다.
  • 그렇기때문에 받아온 데이터토 정렬이 완료된 데이터이다.
  • 이 데이터를 setItems 하면 정렬시키기 끝!

페이징하기

페이징방법 2가지에 대해 알아보자.(여러 방법이 있겠지만)

 

1) offset 기반 pagenation

offset : 상쇄하다 라는 뜻으로 지금까지 받아온 데이터 갯수를 기준으로 잡는다.

1~10의 데이터를 내가 가지고 있으니 11~20까지의 데이터를 달라고 요청하는 것이다.

이 경우 중간에 데이터가 추가되거나 삭제되면 데이터를 중복으로 받아오거나 누락시킬 가능성이있다.

이런 문제점을 보완하기위해 커서기반 페이지네이션을 사용한다.

 

2) cursor기반 pagenation

cursor : 데이터를 가리키는 값. 지금까지 받은 데이터가 어디까지인지 책갈피를 꽂는 것과 같은 동작을 한다.

갯수를 활용한 것이 아니다보니 중복/누락이 발생하지 않는다.

 

하지만 서버입장에서는 cursor 기반 pagenation이 만들이 더 까다롭다고 한다.

그렇기에 data가 자주 바뀌는 것이 아니라면 offset도 충분하다고 한다.

장단점이 있기때문에 무조건 cursor기반 pagenation을 해야하는 것은 아니다.

 

아래는 offset기반 pagenation을 적용한 코드이다.

//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/7087/film-reviews?${query}`
  );
  const body = await response.json();
  return body;
}
// app.js
import React, { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;
function App() {
  const [items, setItems] = useState([]);
  const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(false);
  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 { reviews, paging } = await getReviews(options);
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems([...items, ...reviews]);
    }
    setOffset(options.offset + reviews.length);
    setHasNext(paging.hasNext);
  };

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

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

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

export default App;

app.js 에서 offset, limit, hasNext state를 만든다.

handleLoad함수는 options객체를 받는다.

option.offset === 0  부분은 처음 렌더링했을 때를 말한다.

만약 처음 렌더링 했다면 items는 review로 덮어쓰고, 첫 렌더링이 아니라면 기존의 items에 reviews를 추가할 것이다.

이후 offset과 hasNext를 새로 set해준다.

handelLoadMore함수에서는 order, offset, limit을 넘긴다.

(limit은 고정값으로 사용할 것이기때문에 최상단에 상수로 빼두었다.)

 

useEffect에서도 handleLoad에 파라미터로 { order, offset, limit } 을 넘긴다.

 

그리고 return문에서는

      {hasNext && (
        <button type="button" onClick={handleLoadMore}>
          더보기
        </button>
      )}

이렇게 hasNext가 true면 뒷부분을 렌더링하고, hasNext가 false면 뒷부분을 렌더링 하지 않도록 하는 조건부 렌더링을 추가했다.

 

api.js파일에서는 서버에 request를 보낼 때, offset, limit도 보내도록 수정했다.

 


네트워크 로딩 처리하기

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

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

  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 { reviews, paging } = result;

    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems((prevItems) => [...prevItems, ...reviews]);
    }

    setOffset(options.offset + reviews.length);
    setHasNext(paging.hasNext);
  };

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

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

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

export default App;
  • handleLoad함수에서 result를 let으로 선언해두고, 서버에서 받아오는 값을 result에 저장한다.
  • 이후 result에서 받아온 값을 review, paging에 각각 담도록 수정했다.
  • 그리고 try-catch문으로 네트워크 통신하는 로직을 감쌌다.
  • 통신이 성공하든 실패하든 setIsLoading(false)를 실행해 로딩을 끝낸다.

조건부렌더링

      {hasNext && (
        <button type="button" onClick={handleLoadMore}>
          더보기
        </button>
      )}

위와같이 && 을 사용하면 hasNext가 true일 때 뒷부분을 rendering한다.

|| 을 사용하면 hasNext가 true일 때 rendering하지 않고, false일 때 rendering한다.

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

위와같이 삼항연산자를 활용할 수도 있다.

렌더링되지 않는 값들

// [ 렌더링되지 않는 값들 ]
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;

조건부 렌더링의 주의사항

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인데, 숫자 0은 렌더링이 되는 값이다.

따라서 맨처음 num 이 0으로 넘어오면, 화면에 0이 출력될 수도 있다.

(연산자의 왼쪽값이 리턴된다는 뜻이다.)

 

아래와같이 수정해야한다.

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

react는 true, false값은 렌더링하지 않기때문이다.

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

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

비동기로 state를 변경할 때, 이전 값을 참조하고싶다면 아래의 prevItems처럼 react가 제공해주는 현 시점의 item을 받아와서 사용해야한다.


정리

1. 초깃값 지정

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

콜백으로 초깃값을 지정할수도 있다.

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

콜백안에 getSavedValues를 넣었다.

콜백으로 넣지않고 reviewForm안에 그냥 두면, 렌더링될때 마다 불필요하게 계산할 것이다.

처음 렌더링할 때 한번만 실행되면 되므로, useState의 콜백안에 넣자.

 

2. setter함수 사용

// 원시형 state인 경우
const [state, setState] = useState(0);

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

// 참조형 state인 경우
const [state, setState] = useState({ count: 0 });

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

참조형 state의 경우, 새로운 객체를 생성해줘야한다.

만약 이전 state값을 참조하면서 state를 바꿔야하는 경우, 아래같이 콜백을 사용해 state를 변경해줘야한다.

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

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