export async function getReviews() {
const response = await fetch("api 주소");
const body = await response.json();
return body;
}
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를 사용해서 실행하면 맨 처음 렌더링 될 때만 사용되기 때문에 무한 루프를 막을 수 있다.
const handleLoad = async () => {
const { reviews } = await getReviews();
setItems(reviews);
};
useEffect(() => {
handleLoad();
}, []);
useEffect 함수는 맨처음 렌더링이 끝나면 콜백함수를 실행하고, 그 다음에는 디펜던시 리스트를 비교해서 기억했던 값과 다른 경우에만 실행한다.
useEffect(() => {
handleLoad();
}, [order]);
다음과 같은 경우에서 order state가 바뀔 때마다 재렌더링이 일어난다.
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>
}
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;
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;
잘못된 시점의 값을 사용하는 문제가 있다.
ex) 더보기 누르고 삭제 눌렀는데 렌더링 되면서 삭제된 값이 되살아난 경우
-> 세터함수의 값이 아니라 콜백을 활용
setItems((prevItems) => [...prevItems, ...reviews]);
prevItems 라는 파라미터로 이전 스테이트 값을 받아서 변경할 스테이트 값을 리턴하게 된다.
const [state, setState] = useState(initialState);
useState 함수에 값을 전달하면 초깃값으로 지정할 수 있다.
const [state, setState] = useState(() => {
// 초기값을 계산
return initialState;
});
function ReviewForm() {
const [values, setValues] = useState(() => {
const savedValues = getSavedValues(); // 처음 렌더링할 때만 실행됨
return savedValues
});
// ...
}
이렇게 콜백 형태로 초깃값을 지정해주면
처음 렌더링 할 때 한 번만 콜백을 실행해서 초깃값을 만들고,
그 이후로는 콜백을 실행하지 않기 때문에 getSavedValues 를 불필요하게 실행하지 않는다.
const [state, setState] = useState(0);
const handleAddClick = () => {
setState(state + 1);
}
배열이나 객체 같은 참조형의 경우 반드시 새로운 값을 만들어서 전달해야 된다.
const [state, setState] = useState({ count: 0 });
const handleAddClick = () => {
setState({ ...state, count: state.count + 1 }); // 새로운 객체 생성
}
setState((prevState) => {
// 다음 State 값을 계산
return nextState;
});
이전 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 함수가 실행되도록 하였다.