[React] AbortController와 zustand로 전역 fetch 취소 구현하기 (feat. 카카오 테크)
[Express] 레이어드 아키텍쳐 구현하기 (Layered Architecture)
[Express] 레이어드 아키텍쳐 구현하기 (Layered Architecture)
🚩 소개안녕하세요! 대학생 개발자 주이어입니다!오늘은 레이어드 아키텍쳐 구조화 방법을 사용하여 Express 폴더를 정리해보려고 합니다! 레이어드 아키텍쳐는 실무에서 자주 사용되는 백엔드
blog.juyear.dev
추천 글 읽으러 가기!
🚩 소개
안녕하세요! 대학생 개발자 주이어입니다!
오늘은 AbortController와 zustand를 사용하여 전역으로 fetch 요청을 관리하는 방법에 대해서 정리해 보려고 합니다!
순서는 fetch 요청을 취소해야 하는 이유, Promise에 취소기능이 없는 이유, AbortController에 대해서, zustand 동작 원리, fetch 취소하는 법 순서로 적어보려고 합니다.
⚙️ 불필요한 fetch, 왜 취소해야 할까?
어떻게 보면 너무 당연해보이는 질문일 수 있지만, 제대로 된 이유를 짚고 넘어가보려고 합니다.
await fetch("/userData"); // 간단한 예시
먼저 fetch는 기본적으로 클라이언트에서 서버로 요청을 보낼 때 사용합니다.
만약 위와 같이 입력을 한다면 서버로부터 유저 데이터를 가져오는 fetch문 이구나 라고 예상하실 겁니다.
근데 만약에 사용자가 중간에 다른 페이지로 가거나 또는 더 이상 데이터를 가져올 필요가 없다면 어떻게 될까요?
1. await fetch("userData"); // 서버로 요청 보냄
2. 서버 처리 시작
3. 요청 취소
4. 서버는 이어서 처리
5. 값 반환
위와 같이 진행이 될 것 입니다.
서버는 요청을 받는 순간부터 처리를 시작하며, 중간에 데이터가 필요 없어지더라도 서버는 이를 인식할 수 없기 때문에 이어서 처리를 완료하게 됩니다. 그 후 값을 반환하지만 이미 클라이언트는 필요가 없어진 데이터이죠.
또한 클라이언트는 이 요청을 기다리기 위해 네트워크를 계속해서 할당해야 합니다.
"근데.. 크게 차이가 있나..?"
근데 위와 같이 생각하실 수도 있습니다.
물론, 데이터를 가져오는데 오래걸리지 않는 요청이라면 사용자가 취소를 하기도 전에 이미 반환되어 있을 확률이 높습니다.
하지만 GPT API와 같이 통신하는데 오래 걸리는 요청을 취소하지 않는다면, 불필요한 네트워크 연결이 계속 유지되고,
이후 데이터를 처리하는 후처리 작업까지 이어지면서 클라이언트의 메모리, CPU등을 낭비할 수 있습니다.
만약 현재 서비스를 10000명이 이용 중이고, 이 중 2000명이 데이터가 필요 없어졌다면 클라이언트는 2000명의 불필요한 fetch 요청을 기다리기 위해 계속 네트워크 및 메모리를 낭비하고 있는 것 입니다.
따라서 이러한 fetch문에서는 요청 취소가 필수적이라고 볼 수 있습니다.
🌐 Promise 자체에 취소 기능은 왜 없을까?
AbortController에 대해서 살펴보기 전에 왜 Promise 자체에 취소 기능이 없는지에 대해서 간단하게 알아보려고 합니다.
만약 Promise 및 fetch에 자체 취소 기능이 있었다면 훨씬 더 쉽게 요청을 취소할 수 있었을 겁니다.
하지만 이 기능을 넣기 위해서는 생각보다 고려되야 하는 부분이 많았고, TCP39에서는 이와 관련된 많은 논의를 해왔습니다.
가장 먼저 고려된 부분은 취소 기능의 상태 구분이었습니다.
대기 상태(pending)
이행 상태(fullfilled)
거부 상태(rejected)
Promise는 기본적으로, pending, fullfilled, rejected 3가지 상태로 구분됩니다.
그럼 만약 요청이 취소된다면 fullfilled(이행) 상태일까요? 아니면 rejected(거부) 상태일까요?
취소에 대한 상태 기준이 애매했기 때문에, 처음에는 canceled(취소) 상태를 만들어, 취소 기능을 구현하자는 얘기가 나왔었습니다.
하지만 정말 새로운 상태가 필요한 것인지, 취소는 어떻게 발생시킬 것인지 등에 대해서 끊임없이 논의를 해야 했습니다.
그러던 와중 웹 표준을 담당하는 WHATWG 위원회에서 AbortController를 표준으로 내세우는 일이 발생했습니다.
AbortController에 대해서는 밑에서 더 자세히 설명할 예정이지만, 간단하게 fetch요청에 signal이라는 옵션을 추가로 넣어 요청을 취소할 경우 이 신호를 받아 들여 취소 처리를 진행하도록 해주는 기능이었습니다.
이러다보니 TCP39에서는 기존에 논의 중이던 취소 기능에 대해서 중단하게 되었습니다.
하지만 AbortController는 EventTarget과 DOMException 객체 등에 의존한다는 점 때문에, ECMAScript 언어 표준이 되지 못하였고, TCP39는 이러한 Web 플랫폼에 종속되지 않는 취소 기능의 표준을 만들기 위해서 현재도 논의하고 있는 중입니다.
Promise는 왜 취소가 안 될까? - tech.kakao.com
Promise는 왜 취소가 안 될까? - tech.kakao.com
안녕하세요, 카카오 비즈 FE 파트에서 광고 SDK의 개발을 맡고 있는 Jake입...
tech.kakao.com
이와 관련된 아주 자세한 내용이 카카오 테크에 올라와 있으니 링크를 올려두도록 하겠습니다.
🤔 AbortController는 어떻게 사용할까?
const controller = new AbortController();
const signal = controller.signal;
fetch("/userData", { signal })
.then(res => res.json())
.then(data => {
console.log("데이터 가져오기 성공:", data);
})
.catch(error => {
if (error.name === "AbortError") {
console.log("요청이 중단되었습니다.");
} else {
console.error("다른 오류 발생:", error);
}
});
위 코드는 AbortController로 생성한 signal을 fetch 요청에 같이 보내는 코드 입니다.
이렇게 signal을 같이 보내게 되면, 이후에 abort()로 취소할 경우 signal이 이를 감지하여 요청을 취소하게 됩니다.
setTimeout(() => {
controller.abort(); // 이 시점에서 fetch 요청이 취소됨
console.log("fetch 요청을 중단했습니다.");
}, 3000);
이렇게 위와 같이 controller.abort()를 사용하여 요청을 취소할 수 있습니다.
해당 controller와 연결된 signal에 취소 신호를 보내게 되며, 해당 signal을 사용한 fetch문만 취소됩니다.
주의할 점
AbortController가 요청을 취소하는 방법에 대해서 정확히 이해를 하셔야 하는데,
AbortController는 클라이언트에서 해당 요청을 처리하는 네트워크 자체를 끊어 요청을 취소시키는 방법을 사용하고 있습니다.
따라서 서버 처리와는 관련이 없으며, 서버 메모리 효율과는 관련이 없습니다.
🤔 zustand는 어떻게 사용할까?
Recoil - React v19 호환 오류
zustand 사용법에 대해서 소개하기 전에 먼저 Recoil과 관련해서 소개드릴 부분이 있습니다.
기존에 저는 zustand가 아닌 Recoil을 활용하여 전역 상태를 관리하려고 했고, 실제로 적용이 거의 끝나가는 시점이었습니다.
하지만 마지막 부분에 알 수 없는 오류가 발생했고, 인터넷에 찾아본 결과 Recoil은 React v19에 내부적으로 호환성 문제가 발견되었다고 합니다. 이와 관련된 Github, StackOverflow 글이 많이 있었고 v18로 다운그레이드하는 방법이 있었지만, 저는 Next v15와 함께 RSC 기능을 많이 사용중이었기 때문에 zustand를 사용하게 되었습니다.
React 19 support · Issue #2318 · facebookexperimental/Recoil
React 19 support · Issue #2318 · facebookexperimental/Recoil
It appears recoil does not work with the react 19 RC that came out today. I'm guessing this will not be resolved, since Meta has abandoned this project, but I'd love to be wrong! TypeError: react__...
github.com
위에는 관련 글 링크입니다.
zustand를 다뤄보자
npm install zustand
먼저 zustand를 다운받아 줍니다.
// store.js
import { create } from 'zustand'
export const useCounterStore = create((set) => ({
count: 0,
increase: () => set((state) => ({ count: state.count + 1 })),
decrease: () => set((state) => ({ count: state.count - 1 })),
}))
zustand는 위와 같이 함수형 느낌으로 개발됩니다.
create를 통해 관리할 변수와 함수를 정의해주고, export로 외부에서 사용할 수 있도록 해줍니다.
// App.jsx
import React from 'react'
import useCounterStore from './store'
function App() {
const { count, increase, decrease } = useCounterStore()
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>+1</button>
<button onClick={decrease}>-1</button>
</div>
)
}
그 다음 react에서 방금 생성한 useCounterStore를 가져와준 후, 객체 분해 할당으로 변수와 함수를 가져와줍니다.
그럼 이제 count 변수를 전역으로 관리할 수 있게 됩니다.
그럼 왜 이런 전역 상태 관리 도구를 사용해야 하는지 궁금하실 수 있습니다.
props로 전달하거나 useContext와 같이 기본 도구를 사용하면 되는거 아니냐고 생각할 수 있습니다.
하지만 recoil과 zustand와 같이 전역 상태 관리 도구는 props 전달과 useContext의 단점을 보완해서 사용할 수 있습니다.
props를 통해 상태를 전달할 경우 구조가 복잡해지고 관리가 어려워지는 문제가 있고,
useContext를 사용할 경우 간편하게 상태를 공유할 수 있지만, 상태가 변경될 때 불필요한 리렌더링이 발생할 수 있다는 단점이 있습니다.
이 때문에 recoil이나 zustand와 같은 전역 상태 관리 라이브러리를 사용하면 상태 관리를 더 편리하고 효율적으로 할 수 있습니다.
❌ AbortController + zustand로 요청 취소하기
드디어 오늘 글의 핵심 주제인 AbortController와 zustand를 사용한 전역으로 요청 취소를 관리하는 방법을 소개할 차례입니다.
지금까지 글을 잘 읽어주셨다면, 너무 쉽게 이해하실 겁니다.
예시 코드는 모두 현재 제가 진행 중인 프로젝트의 코드라는 점 미리 말씀드립니다.
import { create } from "zustand";
interface ControllerState {
controller: AbortController | null;
setController: (controller: AbortController | null) => void;
clearController: () => void;
}
export const useControllerStore = create<ControllerState>((set) => ({
controller: null,
setController: (controller) => set({ controller }),
clearController: () => set({ controller: null }),
}));
먼저 zustand로 AbortController을 관리할 수 있는 zustand 스토어를 만들어주었습니다.
- controller
AbortController로 생성된 인스턴스를 저장하는 변수 - setController
입력받은 controller을 변수에 저장하는 함수 - clearController
요청이 끝나거나 취소될 경우 controller 변수를 초기화 해주는 함수
스토어는 위와 같이 구성되어 있습니다.
const newController = new AbortController();
setController(newController);
setClick(true);
if (scheduleId) {
router.replace(`?createSchedule=true&id=${scheduleId}`);
} else {
router.push("?createSchedule=true");
}
setLoading(true);
try {
await fetch("http://localhost:5000/plan/create", {
signal: newController.signal,
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user_message: inputInfo }),
})
위 코드는 Schedule 컴포넌트로, 새로운 controller을 생성하고 setController 함수를 사용해 상태를 저장해주는 코드입니다.
또한 요청을 취소할 수 있도록 fetch 요청에 signal 넣어서 보내주었습니다.
const { controller, clearController } = useControllerStore();
const router = useRouter();
return (
<>
<div
className={style.mainTitle}
onClick={() => {
controller?.abort();
clearController();
router.push("/");
}}
>
TR<span className={style.pointColor}>AI</span>VEL
</div>
...
위 코드는 layout.tsx에서 사용되는 HeaderMenu 컴포넌트로, 상태를 불러와 요청을 취소해주는 코드입니다.
Schedule에서 진행되는 fetch 요청을 HeaderMenu 컴포넌트에서 취소를 할 수 있게 되는 겁니다.
이처럼 zustand와 AbortController을 사용하여 전역으로 요청 취소를 관리할 수 있게 되니다.
😊 마무리
지금까지 정말 긴 글을 작성해보았는데요. 처음에는 AbortController와 zustand를 사용한 요청 취소 전역 관리에 대해서만 작성하려 했지만, "Promise에는 왜 취소 기능이 없을까?" 라는 카카오 테크 글을 보고 관심이 생겨 적다보니 글이 길어지게 됐습니다.
AbortController와 zustand 모두 실무에서 자주 사용되는 매우 중요한 개념이고, 이 2개를 활용하는 요청 취소 전역 관리도 유용하게 활용할 수 있으니 좀 길더라고 한 번씩 읽어주시길 바랍니다.
그럼 지금까지 긴 글 읽어주셔서 감사드리고, 다음에는 더 흥미롭고 유익한 글로 돌아오도록 하겠습니다.
💬 함께 공부하고 싶은 내용이 있다면 댓글이나 피드백으로 알려주세요!
[React] React + Node.js로 사용자별 알림 기능 구현하기 (Notification System)
[React] React + Node.js로 사용자별 알림 기능 구현하기 (Notification System)
[React] 실시간 검색 - debouncing 기능 구현하기 [React] 실시간 검색 - debouncing 기능 구현하기[React, Express] 서버 구현 및 데이터베이스 연결하기(REST API) [React, Express] 서버 구현 및 데이터베이스 연결하
blog.juyear.dev
이전 글 읽으러 가기! 이전 글 읽으러 가기! 이전 글 읽으러 가기! 이전 글 읽으러 가기!
KYT CODING COMMUNITY Discord 서버에 가입하세요!
Discord에서 KYT CODING COMMUNITY 커뮤니티를 확인하세요. 23명과 어울리며 무료 음성 및 텍스트 채팅을 즐기세요.
discord.com
KYT CODING COMMUNITY 가입하기!