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를 실행하면, 콜백함수를 예약해뒀다가 렌더링 이후에 실행하는데 이 때 디펜던시 리스트도 기억해뒀다가 이후 다시 실행되었을 때 이 디펜던시 리스트의 값이 달라졌는지를 확인한다.
위의 예시(백엔드 서버와 연결하기 부분의 예제)에서는 아래와같은 순서로 실행된다.
- 코드를 읽어내리다가 useEffect를 만나 콜백함수를 예약한다.
- 렌더링이 끝나면 콜백함수를 실행한다. 이 때 디펜던시 리스트의 값도 기억해둔다.
- 콜백함수에서 state의 값이 바뀌고 있으니, app.js가 다시 실행된다.
- 다시 useEffect를 만났지만, 현재의 디펜던시리스트와 과거의 디펜던시 리스트를 비교해보니 똑같다.(빈 배열로 똑같다.) 따라서 이번엔 콜백을 예약하지 않는다.
- 렌더링이 마무리된다.
- 콜백함수가 예약되지 않았기 때문에 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);
}
'React' 카테고리의 다른 글
React - 배열을 렌더링시 key를 써야 하는 이유 (0) | 2023.10.23 |
---|---|
React - Virtual DOM (0) | 2023.10.23 |
React - state, react가 render하는 방식 (1) | 2023.10.18 |
React - 예제 프로젝트 dicegame 만들기 (0) | 2023.10.16 |
React - component and props (0) | 2023.10.16 |