포스트

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 서버를 구축한다.

  1. 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);
              
      
  2. 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 }
            );
          }
        );
              
      
  3. 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}`);
     };
        
    
  4. 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 };
     };
        
    
  5. 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 라이센스를 따릅니다.