모킹 서버 작성
MSW는 존재하지 않는 API에 대한 응답을 받을 수 있게 해준다.
개발 환경에서만 사용하므로 --save-dev 옵션을 넣어준다.
npm i msw --save-dev
아래 명령어를 실행하면 public 폴더에 mockServiceWorker.js 파일이 생성된다.
npx msw init public/ --save
가짜 리뷰 데이터 생성을 위해 faker를 설치한다.
npm i @faker-js/faker --save-dev
models/book.model.ts
리뷰의 인터페이스를 정의한다.
export interface BookReviewItem {
id: number;
userName: string;
content: string;
createdAt: string;
score: number;
}
mock/review.ts
리뷰 조회와 리뷰 추가 API를 정의한다. 리뷰는 faker를 사용해서 가짜로 생성해둔다.
import { BookReviewItem } from '@/models/book.model';
import { http, HttpResponse } from 'msw';
import { fakerKO as faker } from '@faker-js/faker';
const mockReviewData: BookReviewItem[] = Array.from({ length: 8 }).map((_, index) => ({
id: index,
userName: `${faker.person.lastName()}${faker.person.firstName()}`,
content: faker.lorem.paragraph(),
createdAt: faker.date.past().toISOString(),
score: faker.helpers.rangeToNumber({ min: 1, max: 5 })
}));
export const reviewsById = http.get('http://localhost:3000/reviews/:bookId', () => {
return HttpResponse.json(mockReviewData, {
status: 200
});
});
export const addReview = http.post('http://localhost:3000/reviews/:bookId', () => {
return HttpResponse.json(
{ message: '리뷰가 등록되었습니다.' },
{ status: 200 }
)
})
mock/browser.ts
아까 만든 API를 등록한다.
import { setupWorker } from 'msw/browser';
import { addReview, reviewsById } from './review';
const handlers = [reviewsById, addReview];
export const worker = setupWorker(...handlers);
main.tsx
msw의 시작 후에 돔이 마운트 되는 게 아니라 둘은 별도로 동작하기 때문에 async 함수로 아래와 같이 작성하여 msw가 시작되기 전에 마운트 되는 것을 막아준다. 이 작업을 하지 않으면 타이밍 때문에 콘솔에서 에러가 발생할 수 있다.
...
async function mountApp() {
if (process.env.NODE_ENV === 'development') {
const { worker } = await import('./mock/browser');
await worker.start();
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
}
mountApp();
리뷰
models/book.model.ts
리뷰를 작성하기 위해 사용자로부터 입력받아야 하는 정보는 리뷰 content와 별점인 score이다.
export type BookReviewItemWrite = Pick<BookReviewItem, 'content' | 'score'>;
review.api.ts
리뷰 목록을 조회 api로 요청을 보내는 함수와 리뷰를 추가하는 api로 요청을 보내는 함수를 정의한다.
export const fetchBookReview = async (bookId: string) => {
return await requestHandler<BookReviewItem>('get', `/reviews/${bookId}`);
}
interface AddBookReviewResponse {
message: string;
}
export const addBookReview = async (bookId: string, data: BookReviewItemWrite) => {
return await requestHandler<AddBookReviewResponse>('post', `/reviews/${bookId}`, data);
}
hooks/useBook.ts
앞서 정의한 함수를 useEffect의 콜백 함수에서 호출하여 reviews에 저장하고, 리뷰 컴포넌트에서 렌더링 할 수 있도록 export 해준다. 리뷰가 추가되면 alert 창으로 리뷰가 등록되었다는 메시지를 보여준다.
export const useBook = (bookId: string | undefined) => {
...
const [reviews, setReviews] = useState<BookReviewItem[]>([]);
...
useEffect(() => {
if (!bookId) return;
fetchBook(bookId).then((book) => {
setBook(book);
});
fetchBookReview(bookId).then((reviews) => {
setReviews(reviews)
})
}, [bookId]);
const addReview = (data: BookReviewItemWrite) => {
if (!book) return;
addBookReview(book.id.toString(), data).then((res) => {
showAlert(res?.message);
})
}
return {..., reviews, addReview };
}
pages/BookDetail.tsx
리뷰가 목차 후에 나타나게 한다.
reviews는 faker로 만들었던 리뷰 배열이고, addReview는 alert 창을 띄우는 함수이다.
function BookDetail() {
...
const { book, likeToggle, reviews, addReview } = useBook(bookId);
...
return (
<BookDetailStyle>
<header className="header">
...
</header>
<div className="content">
...
<Title size='medium'>목차</Title>
<p className="index">{book.contents}</p>
<Title size="medium">리뷰</Title>
<BookReview reviews={reviews} onAdd={addReview} />
</div>
</BookDetailStyle>
);
}
components/book/BookReview.tsx
리뷰 등록 폼 컴포넌트(BookReviewAdd)와 리뷰 하나에 대한 컴포넌트(BookReviewItem)를 map을 이용하여 모두 렌더링 한다.
interface Props {
reviews: IBookReviewItem[];
onAdd: (data: BookReviewItemWrite) => void;
}
function BookReview({ reviews, onAdd }: Props) {
return (
<BookReviewStyle>
<BookReviewAdd onAdd={onAdd} />
{
reviews.map((review) => (
<BookReviewItem review={review} />
))
}
</BookReviewStyle>
);
}
components/book/BookReviewAdd.tsx
리뷰를 등록하는 폼을 가지는 컴포넌트이다.
function BookReviewAdd({ onAdd }: Props) {
const { register, handleSubmit, formState: { errors } } = useForm<BookReviewItemWrite>();
return (
<BookReviewAddStyle>
<form onSubmit={handleSubmit(onAdd)}>
<fieldset>
<textarea {...register('content', { required: true })}></textarea>
{
errors.content &&
<p className="error-text">리뷰 내용을 입력해주세요.</p>
}
</fieldset>
<div className="submit">
<fieldset>
<select {...register('score', { required: true })}>
<option value="1">1점</option>
<option value="2">2점</option>
<option value="3">3점</option>
<option value="4">4점</option>
<option value="5">5점</option>
</select>
</fieldset>
<Button size='medium' scheme="primary">작성하기</Button>
</div>
</form>
</BookReviewAddStyle>
);
}
components/book/BookReviewItem.tsx
별점(Star)은 이 컴포넌트 안에서만 사용되므로 별도로 분리하지 않았다.
상위 컴포넌트에서 받은 리뷰를 렌더링 한다.
interface Props {
review: IBookReviewItem
}
const Star = (props: Pick<IBookReviewItem, 'score'>) => {
return (
<span className="star">
{
Array.from({ length: props.score }, () => (
<FaStar />
))
}
</span>
)
}
function BookReviewItem({ review }: Props) {
return (
<BookReviewItemStyle>
<header className="header">
<div>
<span>{review.userName}</span>
<Star score={review.score} />
</div>
<div>
{formatDate(review.createdAt)}
</div>
</header>
<div className="content">
<p>
{review.content}
</p>
</div>
</BookReviewItemStyle>
);
}
배운 점
- 백엔드 API가 개발되지 않아도 msw 같은 API 모킹 라이브러리를 사용하면 요청을 보내고, 응답을 받을 수 있다.
- PICK을 사용하면 객체에서 특정 타입만 골라서 사용할 수 있다.
- faker를 사용하면 가짜 데이터를 생성할 수 있고, faker에는 한국어용 데이터도 있다.
'데브코스' 카테고리의 다른 글
[16주차 - DAY3] 도서 정보 사이트 - 메인 화면 (1) | 2024.06.12 |
---|---|
[16주차 - DAY2] 도서 정보 사이트 - 다양한 UI (0) | 2024.06.11 |
[15주차 - DAY5] 스니펫 (0) | 2024.06.07 |
[15주차 - DAY5] 도서 정보 사이트 - 리팩토링 (0) | 2024.06.07 |
[15주차 주간 발표] Flex, Grid (0) | 2024.06.05 |