Carrot
Etc/기록들

[TIL] 4월 28일 기록

NaDuck 2023. 4. 29. 02:23

오늘의 할 일

  •  📌 어제자 TIL - [내일 할 일] 확인
  •  📌 TIL 작성
  •  [React로 데이터 다루기] 최대한 집중해서 많이 듣기 하하하하

 

오늘의 나는 무엇을 잘했을까?

  • 오늘자 면접 스터디에서 처음으로 리액트에 관한 질문을 받았는데, 처음 받아보는 질문이어서 버벅거리긴 했지만, 나름대로 논리를 펼쳐서 끝맺음을 잘 하려고 노력했다!💦
  • (+) 습관성 말투로 “이제..”라는 표현을 최대한 쓰지 않으려고 의식하며 답변했다
  • 데일리 스크럼을 성실히 이행했다. 모르는 부분에 대해 팀원 내에서 해결할 수준?에서 소통하려고 노력했고, 또 그 이상으로 어려운 부분은 직접 코드를 쳐가며 실험해서 팀원들과 공유했다.

 

오늘의 나는 무엇을 배웠을까? ⭐(중요체크)

React로 데이터 다루기

useEffect란?

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

useEffect 파라미터

  • 실행할 콜백 함수
  • Defendency list

useEffect를 호출하면 리액트는 곧바로 콜백 함수를 실행하는 게 아니라 콜백 함수를 예약해 뒀다가

  • 첫 렌더링이 끝나고 콜백 함수가 실행됨. 이 때 Defendency list도 같이 기억해둠
  • 그 다음부턴 Defendency list를 비교해서 기억했던 값이랑 다른 경우에만 콜백이 실행됨

 

useEffect를 이용해 state에 따른 서버 데이터 가져오기

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 handleNewestClick = () => setOrder("createdAt");
  const handleBestClick = () => setOrder("rating");

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

  useEffect(() => {
    handleLoad(order);
  }, [order]); // 👈 2번째 파라미터 order state 넣음

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

export default App;
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;
}

export default getReviews;

위 코드에서 useEffect의 dependency list에 order state를 넣으면

이후에 order state의 값이 바뀔 때마다 useEffect에 등록해둔 콜백 함수가 호출되면서 handleLoad 함수 호출됨

ex) 최신순 버튼을 누르면

  1. handleNewestClick 함수로 order의 값이 ‘createApp’으로 설정
  2. order가 바뀌면서 useEffect의 콜백 함수 실행
  3. handleLoad 함수 호출되어 현재 설정된 order를 이용한 쿼리 string을 붙인 url로 fetch 함수 실행
  4. fetch로 가져온 데이터를 item state로 설정
  5. item state가 바뀌면서 재렌더링   ✅ useEffect는 order 값이 바뀔 때만 콜백 함수가 호출되기 때문에 이 때 useEffect 관련해서는 렌더링이 발생하지 않는다.

✅ 첫 렌더링되고 나선 아직 items는 빈 배열인데, useEffect가 호출되면서(처음 렌더링 될 때 한 번 호출되므로) setItems를 호출하기 때문에 재랜더링 발생. 즉 처음 로드하면 2번의 렌더링이 발생함

 

페이지네이션

Q. 페이지에 접속할 때마다 (방대한) 모든 글들을 한 번에 다운로드 받으면 어떻게 될까?

A. 몇 시간씩 기다리거나 아예 못 받을 수 있음. 유튜브 사이트처럼 몇 개의 동영상 데이터를 보여줌. 그리고 더보기 버튼이나 스크롤을 내리면 데이터를 추가로 받아온다. 이런 식으로 데이터를 조금씩 받아오는 것을 페이지네이션이라고 함.

페이지네이션: 책의 페이지처럼 데이터를 나눠서 제공하는 것. 2가지 방식이 있음

  • 오프셋 기반 페이지네이션
  • 커서 기반 페이지네이션
  1. 오프셋 기반 페이지네이션: 지금까지 받아온 데이터 개수
    ex) GET https://…?offset=20&limit=10 = 현재 서버로부터 데이터 20개까지 받았고, 다음 데이터 10개 더 보내줘

    단점: offset 기반으로 데이터를 나누게 되면,
    도중에 서버에서 새로운 데이터가 추가되는 경우, https://…?offset=20&limit=10에서 offset=20은 새로운 데이터가 반영된 전체 데이터에서의 offset으로 설정된다.
    즉 이미 가져온 데이터가 중복되어 가져오는 문제 발생



    서버에서 데이터가 삭제되는 경우엔 일부 데이터를 서버에서 가져오지 못하는 문제 발생

    👇 이러한 오프셋 기반 페이지네이션의 단점을 개선한 것이 커서 기반 페이지네이션

  2. 커서 기반 페이지네이션
    커서(cursor)는 지금까지 받아온 데이터를 가리키는 값


    데이터를 서버로부터 가져오면서 그 response로 보통 paging 정보도 같이 주는데, 그 안에 있는 “nextCursor”가 다음 커서 값을 가리킨다. 이 커서 값으로 request를 보내면 됨.
    ex) GET https://…?cursor=WerZxc&limit=10 = 이 커서 데이터 이후로 데이터 10개 보내줘
    • 만약에 서버에 데이터가 바뀌어도 중복이나 빠짐없이 가져올 수 있다는 장점이 있다.
    • BUT 서버 입장에선 커서 기반이 오프셋 기반보다 만들기 까다롭고, 데이터가 자주 바뀌는 게 아니면 오프셋으로 충분하기 때문

정리) 

  • 오프셋 기반 페이지네이션 = 받아온 개수 기준
  • 커서 기반 페이지네이션 = 데이터를 가리키는 커서 기준

 

실습) 페이지네이션으로 데이터 불러오기

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 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); // offset 값을 불러온 데이터 개수만큼 증가
    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={items} onDelete={handleDelete} />
      {hasNext && <button onClick={handleLoadMore}>더 보기</button>}
    </div>
  );
}

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

핵심정리

  • 페이지네이션으로 한 번 GET 요청할 때 가져올 데이터 개수를 LIMITS 상수로 지정(전역에 선언)
  • 데이터를 처음 불러올 땐 현재 가져온 데이터로 setItems를 호출
  • 이후부턴 기존의 items에 있는 배열(=이전까지 불러온 모든 데이터)에 현재 가져온 데이터를 spread를 이용해 만든 새로운 배열로 setItems 호출
  • offset 페이지네이션 방법으로, offset은 App 컴포넌트에서 state로 관리. 데이터를 가져온 이후에 offset의 값을 증가시켜야 함
  • 최신순, 베스트순 버튼을 번갈아 누르면 기존의 offset이 0으로 초기화되어 다시 처음부터 새로 정렬된 순서대로 데이터를 가져옴
  • 페이지네이션으로 GET요청 시, response에 보통 paging 정보 중 hasNext 값이 있기 때문에 이 값이 false이면 다음 불러올 데이터가 없다는 뜻이다. 따라서 hasNext를 관리하는 state를 만들고, 이 값이 false이면 더보기 버튼이 렌더링되지 않도록 한다.

 

조건부 렌더링

  1. &&, || 연산자 사용하기
    • A && B, A || B 형태에서 앞의 A 부분이 조건문이고, B 부분이 렌더링할 결과물
    • A가 true
      • &&에서 뒤의 값에 따라 결정되므로 렌더링할 결과물이 return
      • ||에서 뒤의 값을 보지 않기 때문에 아무것도 렌더링되지 않음
    • A가 false
      • &&에서 뒤의 값을 보지 않기 때문에 아무것도 렌더링되지 않음
      • ||에서 뒤의 값에 따라 결정되므로 렌더링할 결과물이 return
    ✅ A가 리턴될 때 false 또는 true 값으로 렌더링되지 않는다. (아래 참고)
  2. 삼항 연산자 사용하기 A ? B : C
    • A 조건이 true이면 B를 렌더링, false이면 C를 렌더링 함
    논리 연산자(&&, ||)는 렌더링 할지 안할지이고, 삼항 연산자는 둘 중에 어떤 걸 렌더링할 지 결정할 때 사용

💡 렌더링되지 않는 값들

  • null, undefined
  • true, false
  • ‘’ (빈 문자열)
  • [] (빈 배열)

0, 1은 boolean으로 변환되는 게 아니라, 숫자 그대로 렌더링됨

 

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

function App() {
	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); // offset 값을 불러온 데이터 개수만큼 증가
	  setHasNext(paging.hasNext);
	};
	
	const handleLoadMore = async () => {
	  await handleLoad({ order, offset, limit: LIMIT });
	};

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

더 보기 버튼을 클릭하고 바로 삭제 버튼을 누르면 어떻게 될까?

  • handleLoadMore 함수 호출 → handleLoad 함추 호출
  • handleLoad 함수는 비동기 함수로, 내부에서 const { paging, reviews } = await getReviews(options); 코드를 비동기로 실행
  • 위 코드에서 네트워크를 보내는 동안 삭제 버튼을 누르면, handleDelete 함수 호출되고 실행됨(handleLoad 함수가 비동기 함수이기 때문에 다른 코드가 실행되므로)
  • handleDelete 함수로 인해 items state 값이 변경되고 렌더링됨
  • 하지만 이후 handleLoad 함수에서 await 비동기 작업이 완료된 이후의 items state는 변경된 값이 아님

→ 즉 삭제된 상태로 렌더링이 일어났다가 이전 상태에서(=삭제 전) 더보기가 렌더링되는 문제 발생

→ 비동기로 state를 변경할 때 현재 시점이 아닌, 잘못된 시점의 state를 참조하는 문제가 발생함

 

해결 방법: setter 함수에 값이 아니라 콜백을 전달하기

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); // offset 값을 불러온 데이터 개수만큼 증가
	  setHasNext(paging.hasNext);
	};

콜백 함수를 setter의 파라미터로 넘겨주면, 삭제된 결과의 items state가 반영된다.

 

오늘의 나는 무엇이 궁금했나?

  • 비동기로 state setter를 호출할 때 왜 콜백 함수로 지정해야 실시간 state가 반영되는지 궁금했다. 오늘 데일리 미션에도 나온 주제라서 따로 더 조사해볼 기회가 있었는데, 내가 이해한 선에서 정리해보자면,
    • setter 함수에 값을 그대로 넘겨주면 현재 함수 입장에서 재생성된 const state 값을 참조한다.
    • setter 함수에 콜백으로 넘겨주면, 그 콜백의 파라미터는 state 값이고, 실시간으로 반영된 state로 리액트가 알아서 반영해준다. (왜 이렇게 작동하는지까진 딥하게 공부하는 것 같아서 이정도까지만 알아두는 게 좋을 것 같다)
    import React, { useState } from "react";
    
    function App() {
      const [count, setCount] = useState(0);
      console.log(count);
    
      const handleClick = () => {
        setCount(count + 1); // 1
    
        setCount(count + 1); // 1 
      };
    
      return (
        <div>
          <button onClick={handleClick}>Click Me</button>
        </div>
      );
    }
    
    export default App;
    
    따라서 위의 코드에서 setCount 함수는 state의 값을 그대로 넘겨주기 때문에, 이 때의 count state 값은 항상 0을 참조한다. 만약 1번째 setCount 이후의 count state를 접근하려면 콜백 함수로 지정해야 함

    const handleClick = () => {
        setCount((prevCount) => prevCount + 1); // 1
    
        setCount((prevCount) => prevCount + 1); // 2
      };

    이 때 콜백 함수의 파라미터로 지정한 prevCount는 우리가 설정한 변수 이름으로, 아무 이름으로 지정해도 상관없음. 이 값이 리액트에서 count state의 실시간 값을 가져와 준다는 걸 기억하자.

 

오늘 하루 회고

  • 온라인으로 공부하는 날이라 오프라인 때보다 집중하기 많이 힘든 하루였다💦 힘들긴 했지만 중간중간 팀원들과 데일리 스크럼과 면스를 통해 환기시킨 부분이 좋았고, 앞으로도 이 둘은 더 적극적으로 참여할 것이다🔥🔥🔥
  • 오늘 면스는 특히 더 기억에 남는데, 예상하지 못한 질문을 만나서 당황했지만 최대한 내 논리대로 펼치고 대답하려고 노력한 게 스스로 느껴져서 너무 좋았다. 그럼에도 불구하고 노력한 것과 달리 중간에 버벅 거리고, 답변은 깔끔히 정리하지 못한 채로 말한 것 같아서 피드백을 많이 받을 거라 생각했는데, 내 생각과 달리 팀원들이 좋은 부분을 많이 캐치해줘서 너무 고마웠다💕 물론 아직 많이 부족함을 느끼고 있다..! 그래도 동료들과 같이 으쌰으쌰하고 칭찬해주는 이 분위기가 너무 좋고 만족하고 있다👍

 

'Etc > 기록들' 카테고리의 다른 글

[TIL] 5월 2일 기록  (0) 2023.05.02
[TIL] 5월 1일 기록  (0) 2023.05.02
[WIL] 4월 24일 ~ 4월 30일  (0) 2023.05.01
[TIL] 4월 27일 기록  (0) 2023.04.27
[TIL] 4월 26일 기록  (0) 2023.04.26