2024.07.24 프로그래머스 - React 동적 UI 개발 9
React 로 하는 동적 UI 개발 - 9
Mocking 서버 구축
- msw
- 존재하지 않는 API에 대한 응답을 모킹
service worker 에서 요청을 처리
설치 script
1
npm i msw --save-dev
MSW 사용해보기
1
npx init msw public/ --save
- public 폴더에 msw 서버 코드를 생성
review 라우터에 get 요청을 보내 임의의 데이터를 response로 받는 Mock 서버를 구축한다.
- Mock server
- handler 함수를 통해 요청에 대한 응답을 처리하는 Mock 서버 역할을 한다.
Mock / browser.ts
1 2 3 4 5 6 7
import { setupWorker } from "msw/browser"; import { addReview, reviewsById } from "./reviews"; const handlers = [reviewsById, addReview]; export const worker = setupWorker(...handlers);
- Mock handler
요청에 대한 응답 처리 핸들러 함수를 작성
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
import { BookReviewItem } from "@/shared/models/book.model"; import { fakerKO as faker } from "@faker-js/faker"; import { http, HttpResponse } from "msw"; const mockReviewsData: BookReviewItem[] = Array.from({ length: 8 }).map( (_, idx) => ({ id: idx, 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:8888/reviews/:bookId", () => { const data: BookReviewItem[] = mockReviewsData; return HttpResponse.json(data, { status: 200, }); } ); export const addReview = http.post( "http://localhost:8888/reviews/:bookId", () => { return HttpResponse.json( { message: "리뷰가 등록되었습니다." }, { status: 200 } ); } );
review.api.ts ⇒ fetchBookReview
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import { reviewsById } from "@/mock/reviews"; import { BookReviewItem, BookReviewItemWrite } from "../models/book.model"; import { requestHandler } from "./http"; interface AddBookResponse { message: string; } export const fetchBookReview = async (bookId: string) => { // Route 에서 bookId 수신 return await requestHandler<BookReviewItem>("get", `/reviews/${bookId}`); }; export const addBookReview = async ( bookId: string, data: BookReviewItemWrite ) => { return await requestHandler<AddBookResponse>("post", `/reviews/${bookId}`); };
useBook.ts ⇒ fetchBookReview.then()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
import { useEffect, useState } from "react"; import { Book, BookDetail } from "../shared/models"; import { fetchBook, likeBook, unlikeBook } from "../shared/api/books.api"; import { useAuthStore } from "../store/authStore"; import { useAlert } from "./useAlert"; import { addCart } from "../shared/api/carts.api"; import { BookReviewItem, BookReviewItemWrite, } from "@/shared/models/book.model"; import { addBookReview, fetchBookReview } from "@/shared/api/review.api"; import { useToast } from "./useToast"; export const useBook = (bookId: string | undefined) => { const [book, setBook] = useState<BookDetail | null>(null); const { isLoggedIn } = useAuthStore(); const { showAlert } = useAlert(); const [cartAdded, setCartAdded] = useState<boolean>(false); //review const [reviews, setReviews] = useState<BookReviewItem[]>([]); //... const addReview = (data: BookReviewItemWrite) => { if (!book) return; addBookReview(book.id.toString(), data).then((res) => // fetchBookReview(book.id.toString()).then((reviews) => { // setReviews(reviews) // }) showAlert(res?.message) ); }; useEffect(() => { if (!bookId) return; fetchBook(bookId).then((book) => setBook(book)); fetchBookReview(bookId).then((reviews) => setReviews(reviews)); }, [bookId]); return { book, likeToggle, addToCart, cartAdded, reviews, addReview }; };
BookReview.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
import { BookReviewItemWrite, BookReviewItem as IBookReviewItem, } from "@/shared/models/book.model"; import styled from "styled-components"; import BookReviewItem from "./BookReviewItem"; import BookReviewAdd from "./BookReviewAdd"; interface Props { reviews: IBookReviewItem[]; onAdd: (data: BookReviewItemWrite) => void; } function BookReview({ reviews, onAdd }: Props) { console.log("리뷰", reviews); return ( <BookReviewStyle> <BookReviewAdd onAdd={onAdd} /> {reviews.map((review) => ( <BookReviewItem review={review} /> ))} </BookReviewStyle> ); } const BookReviewStyle = styled.div` display: flex; flex-direction: column; gap: 16px; `; export default BookReview;
외부 근접요소 클릭 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
interface Props {
children: React.ReactNode;
toggleButton: React.ReactNode;
isOpen?: boolean;
}
function Dropdown({ children, toggleButton, isOpen = false }: Props) {
const [open, setOpen] = useState<boolean>(isOpen);
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleOutsideClick(e: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleOutsideClick);
return () => {
document.removeEventListener("mousedown", handleOutsideClick);
};
}, [dropdownRef]);
return (
<DropdownStyle $open={open} ref={dropdownRef}>
<button className="toggle" onClick={() => setOpen(!open)}>
{toggleButton}
</button>
{open && <div className="panel">{children}</div>}
</DropdownStyle>
);
}
interface DropDownStyleProps {
$open: boolean;
}
const DropdownStyle = styled.div<DropDownStyleProps>`
position: relative;
button {
background: none;
border: none;
cursor: pointer;
outline: none;
svg {
width: 30px;
height: 30px;
fill: ${({ theme, $open }) =>
$open ? theme.color.primary : theme.color.text};
}
}
.panel {
position: absolute;
top: 40px;
right: 0;
padding: 16px;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: ${({ theme }) => theme.borderRadius.default};
z-index: 10;
}
`;
export default Dropdown;
useRef 의 ref 기능을 통해, 현재 ref 로 연결된 current가 아닌 부분을 클릭했을때,
핸들러의 인수로 전달받은 이벤트를 통해 분기하여 Toggle 과 같은 기능을 할 수 있다.
1
2
3
4
5
6
7
8
function handleOutsideClick(e: MouseEvent) {
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setOpen(false);
}
}
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.