'비동기 로컬 스토리지'라는 말을 들으면 무엇이 떠오르시나요? 처음에는 이것이 브라우저 기반 로컬 저장소의 마법 같은 구현을 의미한다고 생각할 수도 있습니다. 그러나 이 가정은 올바르지 않습니다. 비동기 로컬 저장소는 브라우저와 관련이 없으며 일반적인 저장소 메커니즘도 아닙니다. 아마도 당신이 사용한 한두 개의 라이브러리가 내부적으로 사용되었을 것입니다. 대부분의 경우 이 기능을 사용하면 지저분한 코드를 처리할 필요가 없습니다.
Async Local Storage는 Node.js에 도입된 기능으로 v13.10.0 및 v12.17.0 버전에 처음 추가되었고 나중에 v16.4.0에서 안정화되었습니다. 이는 Node.js 애플리케이션에서 비동기 리소스를 추적하는 방법을 제공하는 async_hooks 모듈의 일부입니다. 이 기능을 사용하면 여러 비동기 함수가 명시적으로 전달하지 않고도 액세스할 수 있는 공유 컨텍스트를 생성할 수 있습니다. 컨텍스트는 AsyncLocalStorage 인스턴스의 run() 메서드에 전달된 콜백 내에서 실행되는 모든(유일한) 작업에서 사용할 수 있습니다.
예제를 살펴보기 전에 사용할 패턴을 설명해 보겠습니다.
초기화
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
위 모듈에서는 AsyncLocalStorage의 인스턴스를 초기화하고 이를 변수로 내보냅니다.
사용방법
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
run() 메서드는 공유하려는 데이터가 포함된 스토리지와 로직을 배치하는 콜백이라는 두 가지 인수를 사용합니다. 결과적으로 콜백 내의 모든 함수 호출에서 저장소에 액세스할 수 있게 되어 비동기 작업 전반에 걸쳐 원활한 데이터 공유가 가능해집니다.
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
컨텍스트에 액세스하기 위해 인스턴스를 가져오고 asyncLocalStorage.getStore() 메서드를 호출합니다. 좋은 점은 초기화 중에 Context 유형을 AsyncLocalStorage에 전달했기 때문에 getStore()에서 검색된 저장소가 유형화된다는 것입니다: new AsyncLocalStorage
인증 시스템이 없으면 웹 애플리케이션도 없습니다. 인증 토큰을 검증하고 사용자 정보를 추출해야 합니다. 사용자 ID를 얻은 후에는 이를 경로 핸들러에서 사용할 수 있도록 하고 각 ID에서 코드가 중복되는 것을 방지하려고 합니다. AsyncLocalStorage를 활용하여 코드를 깨끗하게 유지하면서 인증 컨텍스트를 구현하는 방법을 살펴보겠습니다.
이 예에서는 fastify를 선택했습니다.
문서에 따르면 fastify는 다음과 같습니다.
빠르고 오버헤드가 낮은 Node.js용 웹 프레임워크
자, 시작해 보겠습니다.
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
npm install fastify
이제 매우 중요한 부분이 나옵니다. authAsyncLocalStorage.run() 메소드로 핸들러를 래핑하기 위해 onRequest 후크를 추가할 예정입니다.
type Context = Map<"userId", string>;
검증에 성공한 후 authAsyncLocalStorage에서 run() 메서드를 호출합니다. 저장소 인수로 토큰에서 검색된 userId와 함께 인증 컨텍스트를 전달합니다. 콜백에서 done 함수를 호출하여 Fastify 수명주기를 계속합니다.
비동기 작업이 필요한 인증 확인이 있는 경우 이를 콜백에 추가해야 합니다. 문서에 따르면 그 이유는 다음과 같습니다.
async/await를 사용하거나 Promise를 반환하는 경우 완료 콜백을 사용할 수 없습니다. 이 상황에서 완료 콜백을 호출하면 예상치 못한 동작이 발생할 수 있습니다. 핸들러 중복 호출
다음은 그 예입니다.
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
이 예에는 보호된 경로가 하나만 있습니다. 더 복잡한 시나리오에서는 특정 경로만 인증 컨텍스트로 래핑해야 할 수도 있습니다. 이러한 경우 다음 중 하나를 수행할 수 있습니다.
좋아요. 컨텍스트가 설정되었으며 이제 보호된 경로를 정의할 수 있습니다.
import Fastify from "fastify"; /* other code... */ const app = Fastify(); function sendUnauthorized(reply: FastifyReply, message: string) { reply.code(401).send({ error: `Unauthorized: ${message}` }); } /* other code... */
코드는 매우 간단합니다. authAsyncLocalStorage를 가져오고, userId를 검색하고, UserRepository를 초기화하고, 데이터를 가져옵니다. 이 접근 방식을 사용하면 경로 핸들러를 깔끔하고 집중적으로 유지할 수 있습니다.
이 예에서는 Next.js의 쿠키 도우미를 다시 구현합니다. 그런데 잠깐만요. 이건 AsyncLocalStorage에 대한 글이겠죠? 그렇다면 쿠키에 대해 이야기하는 이유는 무엇입니까? 대답은 간단합니다. Next.js는 AsyncLocalStorage를 사용하여 서버의 쿠키를 관리합니다. 이것이 바로 서버 구성 요소에서 쿠키를 읽는 것이 다음과 같이 쉬운 이유입니다.
import Fastify from "fastify"; import { authAsyncLocalStorage } from "./context"; import { getUserIdFromToken, validateToken } from "./utils"; /* other code... */ app.addHook( "onRequest", (request: FastifyRequest, reply: FastifyReply, done: () => void) => { const accessToken = request.headers.authorization?.split(" ")[1]; const isTokenValid = validateToken(accessToken); if (!isTokenValid) { sendUnauthorized(reply, "Access token is invalid"); } const userId = accessToken ? getUserIdFromToken(accessToken) : null; if (!userId) { sendUnauthorized(reply, "Invalid or expired token"); } authAsyncLocalStorage.run(new Map([["userId", userId]]), async () => { await new Promise((resolve) => setTimeout(resolve, 2000)); sendUnauthorized(reply, "Invalid or expired token"); done(); }); }, ); /* other code... */
우리는 쿠키 관리를 위한 여러 방법을 제공하는 next/headers에서 내보낸 쿠키 기능을 사용합니다. 그런데 이것이 기술적으로 어떻게 가능할까요?
먼저 이 예는 Lee Robinson의 훌륭한 비디오와 Next.js 저장소에 대한 다이빙을 통해 얻은 지식을 기반으로 한다는 점을 말씀드리고 싶습니다.
이 예에서는 Hono를 서버 프레임워크로 사용합니다. 제가 선택한 이유는 두 가지입니다.
Hono를 먼저 설치하세요.
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
이제 Hono를 초기화하고 미들웨어를 추가합니다.
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
코드가 Fastify 예제의 미들웨어와 비슷하죠? 컨텍스트를 설정하기 위해 우리는 쿠키 모듈에서 가져온 setCookieContext(쿠키 기능의 사용자 정의 단순 구현)를 활용합니다. setCookieContext 함수를 따라가서 가져온 모듈로 이동해 보겠습니다.
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
setCookieContext 함수(Hono 미들웨어의 cookieAsyncLocalStorage.run()에 반환 값을 전달함)는 hono 컨텍스트를 나타내는 c 매개변수에서 쿠키를 추출하고 이를 쿠키 관리를 위한 유틸리티 기능을 제공하는 클로저와 함께 묶습니다.
저희 쿠키 기능은 next/headers의 쿠키 기능을 복제합니다. cookieAsyncLocalStorage.getStore() 메서드를 활용하여 호출 시 cookieAsyncLocalStorage.run()에 전달되는 동일한 컨텍스트에 액세스합니다.
Next.js 구현의 동작을 모방하겠다는 약속으로 쿠키 기능 반환을 래핑했습니다. 버전 15 이전에는 이 기능이 동기식이었습니다. 이제 현재 Next.js 코드에서는 다음의 간단한 예와 같이 쿠키에서 반환된 메서드가 Promise 개체에 연결됩니다.
npm install fastify
언급할 만한 또 다른 점은 우리의 경우 cookie.setCookie 및 cookie.deleteCookie를 사용하면 서버 구성 요소에서 쿠키를 설정할 때 Next.js에서 관찰되는 동작과 유사하게 항상 오류가 발생한다는 것입니다. 원래 구현에서 setCookie 또는 deleteCookie를 사용할 수 있는지 여부는 RequestStore(AsyncLocalStorage의 구현이며 쿠키도 저장함)라는 저장소에 저장된 단계(WorkUnitPhase) 속성에 따라 달라지기 때문에 이 논리를 하드코딩했습니다. 그러나 해당 주제는 다른 게시물에 더 적합합니다. 이 예를 단순하게 유지하기 위해 WorkUnitPhase의 시뮬레이션을 생략하겠습니다.
이제 React 코드를 추가해야 합니다.
type Context = Map<"userId", string>;
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
쿠키의 사용법은 Next.js React 서버 구성요소에서 사용되는 방식과 유사합니다.
import Fastify from "fastify"; /* other code... */ const app = Fastify(); function sendUnauthorized(reply: FastifyReply, message: string) { reply.code(401).send({ error: `Unauthorized: ${message}` }); } /* other code... */
우리 템플릿은 hono 컨텍스트에서 html 메소드로 렌더링됩니다. 여기서 중요한 점은 경로 핸들러가 cookieContext를 사용하는 asyncLocalStorage.run() 메서드 내에서 실행된다는 것입니다. 결과적으로 쿠키 기능을 통해 DisplayCookies 구성 요소에서 이 컨텍스트에 액세스할 수 있습니다.
React 서버 구성 요소 내부에서는 쿠키를 설정할 수 없으므로 수동으로 설정해야 합니다.
페이지를 새로 고치세요:
이제 쿠키가 성공적으로 검색되어 표시되었습니다.
asyncLocalStorage에는 더 많은 사용 사례가 있습니다. 이 기능을 사용하면 거의 모든 서버 프레임워크에서 사용자 정의 컨텍스트를 구축할 수 있습니다. asyncLocalStorage 컨텍스트는 run() 메서드 실행 내에 캡슐화되어 관리가 쉽습니다. 요청 기반 시나리오를 처리하는 데 적합합니다. API는 간단하고 유연하며 각 상태에 대한 인스턴스를 생성하여 확장성을 가능하게 합니다. 인증, 로깅, 기능 플래그 등을 위한 별도의 컨텍스트를 원활하게 유지할 수 있습니다.
이점에도 불구하고 명심해야 할 몇 가지 고려 사항이 있습니다. asyncLocalStorage가 코드에 너무 많은 '마법'을 도입한다는 의견을 들었습니다. 이 기능을 처음 사용했을 때 개념을 완전히 이해하는 데 시간이 좀 걸렸다는 점을 인정하겠습니다. 고려해야 할 또 다른 사항은 컨텍스트를 모듈로 가져오면 관리해야 하는 새로운 종속성이 생성된다는 것입니다. 그러나 결국 깊게 중첩된 함수 호출을 통해 값을 전달하는 것은 훨씬 더 나쁩니다.
읽어주셔서 감사하고, 다음 포스팅에서 만나요!?
PS: 여기에서 예시(보너스 1개 포함)를 찾을 수 있습니다
보그 포스트 출처: https://www.aboutjs.dev/en/async-local-storage-is-here-to-help-you
위 내용은 비동기 로컬 저장소가 여러분을 도와드립니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!