오늘의 할 일
- ✅ 📌 어제자 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) 최신순 버튼을 누르면
- handleNewestClick 함수로 order의 값이 ‘createApp’으로 설정
- order가 바뀌면서 useEffect의 콜백 함수 실행
- handleLoad 함수 호출되어 현재 설정된 order를 이용한 쿼리 string을 붙인 url로 fetch 함수 실행
- fetch로 가져온 데이터를 item state로 설정
- item state가 바뀌면서 재렌더링 ✅ useEffect는 order 값이 바뀔 때만 콜백 함수가 호출되기 때문에 이 때 useEffect 관련해서는 렌더링이 발생하지 않는다.
✅ 첫 렌더링되고 나선 아직 items는 빈 배열인데, useEffect가 호출되면서(처음 렌더링 될 때 한 번 호출되므로) setItems를 호출하기 때문에 재랜더링 발생. 즉 처음 로드하면 2번의 렌더링이 발생함
페이지네이션
Q. 페이지에 접속할 때마다 (방대한) 모든 글들을 한 번에 다운로드 받으면 어떻게 될까?
A. 몇 시간씩 기다리거나 아예 못 받을 수 있음. 유튜브 사이트처럼 몇 개의 동영상 데이터를 보여줌. 그리고 더보기 버튼이나 스크롤을 내리면 데이터를 추가로 받아온다. 이런 식으로 데이터를 조금씩 받아오는 것을 페이지네이션이라고 함.
페이지네이션: 책의 페이지처럼 데이터를 나눠서 제공하는 것. 2가지 방식이 있음
- 오프셋 기반 페이지네이션
- 커서 기반 페이지네이션
- 오프셋 기반 페이지네이션: 지금까지 받아온 데이터 개수
ex)GET https://…?offset=20&limit=10
= 현재 서버로부터 데이터 20개까지 받았고, 다음 데이터 10개 더 보내줘
단점: offset 기반으로 데이터를 나누게 되면,
도중에 서버에서 새로운 데이터가 추가되는 경우,https://…?offset=20&limit=10
에서 offset=20은 새로운 데이터가 반영된 전체 데이터에서의 offset으로 설정된다.
즉 이미 가져온 데이터가 중복되어 가져오는 문제 발생
서버에서 데이터가 삭제되는 경우엔 일부 데이터를 서버에서 가져오지 못하는 문제 발생
👇 이러한 오프셋 기반 페이지네이션의 단점을 개선한 것이 커서 기반 페이지네이션 - 커서 기반 페이지네이션
커서(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이면 더보기 버튼이 렌더링되지 않도록 한다.
조건부 렌더링
&&
,||
연산자 사용하기A && B
,A || B
형태에서 앞의 A 부분이 조건문이고, B 부분이 렌더링할 결과물- A가
true
&&
에서 뒤의 값에 따라 결정되므로 렌더링할 결과물이 return||
에서 뒤의 값을 보지 않기 때문에 아무것도 렌더링되지 않음
- A가
false
&&
에서 뒤의 값을 보지 않기 때문에 아무것도 렌더링되지 않음||
에서 뒤의 값에 따라 결정되므로 렌더링할 결과물이 return
- 삼항 연산자 사용하기
A ? B : C
- A 조건이
true
이면 B를 렌더링,false
이면 C를 렌더링 함
- A 조건이
💡 렌더링되지 않는 값들
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로 리액트가 알아서 반영해준다. (왜 이렇게 작동하는지까진 딥하게 공부하는 것 같아서 이정도까지만 알아두는 게 좋을 것 같다)
따라서 위의 코드에서 setCount 함수는 state의 값을 그대로 넘겨주기 때문에, 이 때의 count state 값은 항상 0을 참조한다. 만약 1번째 setCount 이후의 count 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;
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 |