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;
'react' 카테고리의 다른 글
[코드잇] 리액트로 데이터 다루기 (4) (0) | 2023.09.24 |
---|---|
[코드잇] 리액트로 데이터 다루기(3) (0) | 2023.09.04 |
[코드잇] 리액트로 데이터 다루기(1) (0) | 2023.09.02 |
[코드잇] 리액트 웹 개발 시작하기 (6) (0) | 2023.09.02 |
[코드잇] 리액트 웹 개발 시작하기 (5) (1) | 2023.09.02 |