모킹 서버 작성 MSW
자바스크립트 라이브러리. 업계표준라이브러리. 자바스크립트라면 다른 환경에서도 많이 사용한다!
Mocking responses 모킹 : 실제로 존재하지 않는 것을 가상으로 구현하는 것. node 서버를 가상으로 띄워논다
- Mock Service Worker
- 존재하지 않는 API 에 대한 응답을 모킹
- service worker에서 요청을 처리
- chrome 기준 devTool의 Application / service workers의 "Bypass for network"로 일시정지
실무에서는 로그에 남기 때문에, 껏다켜기 어렵다.
리뷰 처리
목 서비스로 리뷰 생성하기
리뷰 모델, 테스트 생성
export interface BookReviewItem {
id: number;
userName: string;
content: string;
createAt: string;
score: number;
}
//..
export const fetchBookReview = async(bookId:string)=>{
return await requestHandler<BookReviewItem>("get",`/reviews/${bookId}`)
}
//..
const [reviews, setReviews] = useState<BookReviewItem[]>([]);
fetchBookReview(bookId).then((reviews) => {
setReviews(reviews);
});
//..
const { book, likeToggle, reviews } = useBook(bookId);
MSW 적용
`npm i msw --save-dev`
로컬 생성 `npx msw init public/ --save` public 지정 이유는 spa 서빙하는 파일이 존재하는데, 브라우저에 모킹서버를 직접 동작하겠다는 의미이다.
`mockServiceWorker.js` 파일 생성된다.
browser.ts 에 목 브라우저 불러와서 사용하는 코드
import { setupWorker } from 'msw/browser';
const handlers = [];
export const worker = setupWorker(...handlers);
review.ts
import { http, HttpResponse } from 'msw';
import { BookReviewItem } from '../models/book.model';
export const reviewsById = http.get(
'https://localhost:2222/reviews/:bookId',
() => {
const data: BookReviewItem[] = [];
return HttpResponse.json(data, {
status: 200,
});
},
);
* 비동기 처리의 타이밍 이슈 해결방법
DOM 의 마운트하는 부분을 한번에 묶어서 async 로 요청을 보낸다.
async function mountApp() {
if (process.env.NODE_ENV === 'development') {
const { worker } = require('./mock/browser');
await worker.start(); //이때 시작
}
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}
mountApp();
faker.js 더미데이터
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(),
createAt: faker.date.past().toISOString(),
score: faker.helpers.rangeToNumber({ min: 1, max: 5 }),
}),
);
helpers에 range 많이 사용함. 8개의 배열 데이터
fakerKO 한국어 서비스
UI
드롭다운
유저아이콘 > 하단 패널 등장
옵션 모달 생성, 공간을 유용하고, 사용자 상호작용에 유용하다 <> selector 와 다르다
click outside pattern
패널 밖모습 클릭하면 창 닫힘
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]);
//..
<DropdownStyle $open={open} ref={dropdownRef}>
if 문 안의 조건이 많이쓰이는 패턴이다!
dropdownRef 가 현재 참이고 && 클릭했을때, 컨테인이 포함하는 부분 외부일 경우! setOpen을 false 로 설정
탭
<Tabs> 라이브러리 없이 구현하기
//자식탭
interface TabsProps {
children: React.ReactNode;
}
const Tabs = ({ children }: TabsProps) => {
const [activeIndex, setActiveIndex] = useState(0);
const tabs = React.Children.toArray(
children,
) as React.ReactElement<TabProps>[];
return (
<TabsStyle>
<div className="tab-header">
{tabs.map((tab, index) => (
<button
onClick={() => setActiveIndex(index)}
className={activeIndex === index ? 'active' : ''}
>
{tab.props.title}
</button>
))}
</div>
<div className="tab-content">{tabs[activeIndex]}</div>
</TabsStyle>
);
};
//BookDetail.tsx
<div className="content">
<Tabs>
<Tab title="상세설명">
<Title size="medium">상세 설명</Title>
<EllipsisBox linelimit={4}>{book.detail}</EllipsisBox>
</Tab>
<Tab title="목차">
<Title size="medium">목차</Title>
<p className="index">{book.contents}</p>
</Tab>
<Tab title="리뷰">
<Title size="medium">리뷰</Title>
<BookReview reviews={reviews} onAdd={addReview} />
</Tab>
</Tabs>
</div>
activeIndex를 이용해서 상태를 관리한다.
children을 받아서 React.Children.toArray 메서드로 tab 변수에 할당한다. 그리고 map 으로 순회하면서 상태관리한다.
토스트 showAlert
여러개의 토스트를 열릴 수 있도록, zustand 로 전역 상태관리를 한다.
toastStore.ts
const useToastStore = create<ToastStoreState>((set) => ({
toasts: [],
addToast: (message, type = 'info') => {
set((state) => ({
toasts: [...state.toasts, { message, type, id: Date.now() }],
}));
},
removeToast: (id) => {
set((state) => ({
toasts: state.toasts.filter((toast) => toast.id !== id),
}));
},
}));
hook으로 만들어서 사용한다.
export const useToast = () => {
const showToast = useToastStore((state) => state.addToast);
return { showToast };
};
Toast 는 사이트 전역에서 표시되어야 하기 때문에,
컨테이너를 생성해서 상위App.tsx에 넣어주어야 한다.
const ToastContainer = () => {
const toasts = useToastStore((state) => state.toasts);
return (
<ToastContainerStyle>
{toasts.map((toast) => (
<Toast
key={toast.id}
id={toast.id}
message={toast.message}
type={toast.type}
/>
))}
</ToastContainerStyle>
);
};
3초 뒤에 fadeout 되면서 사라짐
`onAnimationEnd`
useEffect(() => {
const timer = setTimeout(() => {
//delete
handleRemoveToast();
}, TOAST_REMOVE_DELAY);
return () => clearTimeout(timer);
}, []);
const handleAnimationEnd = () => {
if (isFadingOut) {
removeToast(id);
}
};
//..
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
&.fade-in {
animation: fade-in 0.3s ease-in-out forwards;
}
&.fade-out {
animation: fade-out 0.3s ease-in-out forwards;
}
opacity: 0;
transition: all 0.3s ease-in-out;
이 코드 timeout 을 많이 사용하기 때문에, hook 으로 분리해서 사용하는게 좋다.
export const useTimeout = (callback: () => void, delay: number) => {
useEffect(() => {
const timer = setTimeout(callback, delay);
return () => clearTimeout(timer);
}, [callback, delay]);
};
export default useTimeout;
//...
useTimeout(() => {
setIsFadingOut(true);
}, TOAST_REMOVE_DELAY);
모달
modal-body, modal-contents 구조로 나뉘고, contents 내부에 자식요소를 제공한다.
top, left 를 50%로 지정하면, div의 왼쪽상단 모서리가 기준이기 때문에, `transform: translate(-50%, -50%);` 처럼 div 너비의 절반으로 중앙정렬할 수 있다.
x, 외부 클릭시 닫힘, 이미지 클릭시 열림
+ ESC 누르면 닫힘
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
handleClose();
}
};
useEffect(() => {
if (isOpen) {
window.addEventListener('keydown', handleKeydown);
} else {
window.removeEventListener('keydown', handleKeydown);
}
return () => {
window.removeEventListener('keydown', handleKeydown);
};
}, [isOpen]);
이벤트리스너 등록하고, key값에 엑세스해서 키에 대한 이벤트 분기하는 부분 체크하기!
모달(토스트)의 위치는 header 의 아래가 아닌, 전체 root 의 바로 아래에 위치해야한다.
> 리액트 create portal 기능(react-dom)
import { createPortal } from 'react-dom';
//..
return createPortal(
<ModalStyle
className={isFadingOut ? 'fade-out' : 'fade-in'}
onClick={handleOverlayClick}
onAnimationEnd={handleAnimationEnd}
>
<div className="modal-body" ref={modalRef}>
<div className="modal-contents">{children}</div>
<button className="modal-close" onClick={handleClose}>
<FaPlus />
</button>
</div>
</ModalStyle>,
document.body,
);
무한 스크롤
react-query의 infinite queries 기능을 제공한다!
return의 구조의 변화 `flatMap` 사용
결과 = {
books, pagination
}
-----------------------
결과 = {
pages: [
{books, pagination},
{books, pagination},
{books, pagination},
]
}
const books = data ? data.pages.flatMap((page) => page.books) : [];
const pagination = data ? data.pages[data.pages.length - 1].pagination : {};
const isEmpty = books.length === 0;
return {
books,
pagination,
isEmpty,
isBooksLoading: isFetching,
fetchNextPage,
hasNextPage,
};
더보기 감지 IntersectionObserver API 방식
뷰포트와 타겟과 교차했을때 특정값을 return 해준다.
이벤트에 붙는게 아닌, 지속석으로 reActive 상태처럼 동작한다.
특정 돔이 화면에 등장한다면, fetchNext 가 동작하도록!
more 컨테이너를 관찰하고 있다가, 해당 컨테이너가 보인다면 isIntersecting 한다면 (뷰포트와 타겟의 교차) 사용자 눈 화면에 보인다면?, 다음페이지를 로드한다.
interface ObserverOptions {
root?: Element | null;
rootMargin?: string;
threshold?: number | number[]; //0 or [0,1]
}
root : 관찰되는 대상의 기준점. 요소일 수도 있고, 없을수도 있다.
rootMargin : 교차되는 거리
threshold : 교차의 비율. 타겟값 요소가 화면에 얼만큼 보일때 할것인지 0에서 ~1까지
export const useIntersectionObserver = (
callback: Callback,
options?: ObserverOptions,
) => {
const targetRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(callback, options);
if (targetRef.current) {
observer.observe(targetRef.current);
}
return () => {
if (targetRef.current) {
observer.unobserve(targetRef.current);
}
};
});
return targetRef;
};
return이 꼭 필요함!
훅 호출 후 사용법
const moreRef = useIntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
loadMore();
}
});
const loadMore = () => {
if (!hasNextPage) return;
fetchNextPage();
};
☑️ 배운 점
라이브러리가 아닌, UI 구현을 직접 할 수 있어서 좋았음.
기능구현 과제 중에는 라이브러리 아닌 직접구현 한다면 가산점이 생긴다던데,,
모달, 탭, 드롭다운의 경우 직접 구현한 바 있어서 복습해서 좋았다.
토스트는 react-toast 를 썼었는데, 라이브러리 아닌 직접 구현도 생각보다 복잡하지 않았다.
무한스크롤은 와 어려웠다. react-query의 Infinity queries 를 사용하는 방법1, IntersectionObserver 로 스크롤인식하는 방법2
react-query를 이용해서 데이터 가공한다면, 사용해도 좋은 방식, 아니라면 IntersectionObserver를 사용하는것이 좋아보인다.