diff --git "a/content/blog/frontend/\355\216\200\354\236\207_\353\254\264\355\225\234\354\212\244\355\201\254\353\241\244.md" "b/content/blog/frontend/\355\216\200\354\236\207_\353\254\264\355\225\234\354\212\244\355\201\254\353\241\244.md"
new file mode 100644
index 0000000..cf50d83
--- /dev/null
+++ "b/content/blog/frontend/\355\216\200\354\236\207_\353\254\264\355\225\234\354\212\244\355\201\254\353\241\244.md"
@@ -0,0 +1,204 @@
+---
+title: '펀잇팀의 무한스크롤 개발기'
+date: 2023-10-15 15:40:00
+category: 'frontend'
+draft: false
+---
+
+```
+📢 이 글은 무한스크롤을 구현하는 방법보다는 구현 과정에서 있었던 일에 대해 담고 있습니다.
+```
+
+펀잇 서비스에는 ‘상품 목록’을 보여주는 화면과 상품 상세 페이지에서 ‘리뷰 목록’을 보여주는 화면이 있다. 이러한 긴 목록들을 보여주는 방법에는 페이지네이션과 무한스크롤이 있다. 그 중에서 우리 팀은 무한스크롤 방식을 선택했고 왜 이 방식을 선택했는지, 그리고 어떤 방식으로 구현 했는지 글을 작성해보려고 한다.
+
+## 페이지네이션
+
+
+
+페이지네이션은 화면 하단의 숫자를 클릭하여 선택한 페이지 넘버에 해당하는 페이지를 선택하는 방식이다. 대표적으로 구글이 이런 방식을 채택하고 있다. (요즘에는 더보기 버튼이 보이는 것 같기도..?)
+
+일정한 양을 매번 보여줌으로써 사용자들이 원하는 컨텐츠의 위치를 파악할 수 있도록 도와준다. 페이징이 일어날 때마다 사용자는 ‘클릭’이라는 행동을 취해야 한다는 특징이 있다.
+
+## 무한스크롤
+
+![ezgif com-video-to-gif (4)](https://github.com/woowacourse-teams/2023-fun-eat/assets/55427367/e3c98015-497c-4bbc-8613-f450200dbc48)
+
+무한스크롤은 사용자가 스크롤을 내릴 때 페이징이 일어나는 방식이다. 사용자들이 직접 선택한 페이지로 이동하는 것은 불가능하지만, 스크롤만 해도 상품들이 더 노출되어 사용자의 행동을 최소화해준다는 장점이 있다.
+
+어떤 UX를 고를 것인지는 서비스의 의도, 환경 등에 따라 달라지게 된다고 생각한다.
+
+이런 무한스크롤도 구현 방법이 나뉘는데 scroll event를 사용하는 방법과 intersection observer을 사용하는 방법으로 나뉜다.
+
+scroll event는 매번 스크롤 이벤트가 일어날 때마다 높이를 감지하는 방식이고 intersection observer는 교차하는 지점을 감지하여 무한스크롤을 하는 방식이다.
+
+최종적으로 우리 팀에서는 intersection observer를 사용한 무한스크롤 방식을 채택하게 되었는데 그 이유는 아래와 같다.
+
+1. 펀잇은 모바일 기준으로 제작되어 클릭 액션을 최소화한 무한스크롤이 더 좋은 사용자 경험을 제공할 것임
+2. 펀잇에는 수천개의 상품이 존재하는데 이를 내릴 때마다 스크롤 이벤트가 발생하게 된다면 페이지 부하가 심해질 것임
+
+## 구현 방법
+
+### useIntersectionObserver hook 작성
+
+1. options
+
+```tsx
+const defaultOptions = {
+ root: null,
+ rootMargin: '0px',
+ threshold: 0.3,
+}
+```
+
+- `root`: 타겟 요소의 가시성을 검사할 때 사용하는 루트 요소 (`null` 일 때 브라우저 뷰포트로 설정)
+- `rootMargin`: 루트요소의 범위를 확장하여 확장된 영역에 들어가면 가시성 변화 발생하게 한다. 단위를 필수적으로 적어줘야 한다.
+- `threshold`: 요소가 어느정도로 보여졌을 때 콜백을 실행할 것인지 결정
+
+1. observer 생성
+
+```tsx
+const observer = useRef(
+ new IntersectionObserver(entries => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting) {
+ callback()
+ }
+ })
+ }, defaultOptions)
+)
+```
+
+1. observe, unobserve 함수 작성
+
+```tsx
+const observe = (element: T) => {
+ observer.current.observe(element);
+};
+
+const unobserve = (element: T) => {
+ observer.current.unobserve(element);
+};
+
+useEffect(() => {
+ if (!targetRef.current) {
+ return;
+ }
+
+ if (isLastPage) {
+ unobserve(targetRef.current);
+ return;
+ }
+
+ observe(targetRef.current);
+
+ return () => {
+ observer.current.disconnect();
+ };
+ }, [targetRef.current]);
+};
+```
+
+### 완성본
+
+```tsx
+import type { RefObject } from 'react';
+import { useRef, useEffect } from 'react';
+
+const defaultOptions = {
+ root: null,
+ rootMargin: '0px',
+ threshold: 1.0,
+};
+
+const useIntersectionObserver = (
+ callback: () => void,
+ targetRef: RefObject,
+ isLastPage: boolean | undefined
+) => {
+ let isInitial = true;
+
+ const observer = useRef(
+ new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ if (isInitial) {
+ isInitial = false;
+ } else {
+ callback();
+ }
+ }
+ });
+ }, defaultOptions)
+ );
+
+ const observe = (element: T) => {
+ observer.current.observe(element);
+ };
+
+ const unobserve = (element: T) => {
+ observer.current.unobserve(element);
+ };
+
+ useEffect(() => {
+ if (!targetRef.current) {
+ return;
+ }
+
+ if (isLastPage) {
+ unobserve(targetRef.current);
+ return;
+ }
+
+ observe(targetRef.current);
+
+ return () => {
+ observer.current.disconnect();
+ };
+ }, [targetRef.current]);
+};
+```
+
+## 트러블슈팅
+
+펀잇에는 여러개의 카테고리가 존재한다. 일반 상품 5가지, PB 상품 4가지로 총 9가지의 탭에서 모두 무한스크롤이 일어나게 된다. 따라서 사용자가 카테고리 탭을 바꿀 때마다 `page = 0` 으로 초기화해서 요청을 보내야했다.
+
+이 때, page 값 초기화와 비동기 요청의 순서를 보장할 수 없는 문제가 있었다. 예를 들어 간편식사 탭에서 `page = 4` 까지의 요청을 보낸 뒤 과자류 탭으로 이동했을 때 `page = 4` 의 요청이 먼저 간 뒤 `page = 0` 요청이 일어나는 것이었다.
+
+도대체 왜 이런 현상이 일어나는 것일까 고민해봤는데 내가 생각한 문제점은 다음과 같았다.
+
+무한스크롤을 할 때 두 가지의 작업을 하게 된다.
+
+1. `setPage` 를 통해 page를 초기화하는 과정
+
+2. 팀 내에서 만든 `useGet` 을 통해 data를 가져오는 과정 → 이 때 의존성 배열 `[categoryId, page]` 가 존재한다.
+
+이때 page 상태가 0으로 초기화되기 전에 API 요청이 완료되어 상품 목록을 응답하게 되면 두 과정 사이의 race condition이 발생하는 것으로 판단했습니다. setState를 하는 함수는 렌더링 동일성이 보장되지만 비동기 요청은 순서를 보장하지 않기 때문이다.
+
+**🤔 해결방안**
+
+문제를 해결하기 위해 생각했던 방법은 카테고리값이 변경된 것을 확인하고 요청을 보내는 것이었다. 공식 문서에서 ref는 어떠한 정보를 저장하고 싶지만 렌더는 일으키고 싶지는 않을 때도 사용한다는 내용을 참고해 해당 방법으로 접근했다.
+
+`previousCategoryId`를 ref로 값을 저장
+
+그 후 `categoryId`와 비교해서 값이 바뀌었다면 `page`를 초기화 한 후 비동기 요청을 보내게끔 구현
+
+```tsx
+const prevCategoryId = useRef(categoryId)
+const nextPage = prevCategoryId.current !== categoryId ? 0 : page
+
+useEffect(() => {
+ setPage(0) //초기화
+ prevCategoryId.current = categoryId
+}, [categoryId, sort])
+
+// useCategoryProducts에 전달해주는 값들이 refetch 조건임
+const { data: productListResponse } = useCategoryProducts(
+ categoryId,
+ nextPage,
+ sort
+) //비동기 요청
+```
+
+물론 이 문제는 [Suspense](https://react.dev/blog/2022/03/29/react-v18#new-suspense-features)를 사용하면 해결이 가능하다.
+
+이에 대해서는 2탄으로 이어적어보도록 하겠다!