Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

Remix에서 Upstash Redis를 세션 저장소로 활용하는 방법

풀스택 웹 프레임워크인 Remix는 웹 서버에서 자주 발생하는 일반적인 사용 사례들을 처리할 수 있는 API를 제공합니다. 이 글에서는 그중 세션(Session)에 초점을 맞추고, 왜 그리고 어떻게 Upstash를 세션 저장소로 사용할 수 있는지 알아보겠습니다.

세션이란 무엇인가?

Remix 공식 문서에 세션에 대한 훌륭한 소개가 있으니 참고하세요: https://remix.run/docs/en/v1/api/remix#sessions

간단히 말해, 세션은 서버와 클라이언트가 사용자 데이터나 상태(state)를 공유할 수 있도록 해주는 메커니즘입니다. 대표적인 세션 활용 사례로는 사용자 인증 상태 추적, 장바구니 상태 관리, 플래시 메시지(일회성 알림) 등이 있습니다.

왜 Upstash Redis를 사용해야 할까?

세션 데이터는 서버에 저장됩니다. 하지만 서버리스(Serverless) 인프라나 PaaS 인프라(예: Heroku)에 배포하면 서버의 파일 시스템을 영구적으로 사용할 수 없습니다. 서버리스 환경에서는 요청마다, PaaS에서는 배포할 때마다 파일 시스템이 초기화될 수 있기 때문입니다. 따라서 데이터를 영속화하려면 외부 데이터베이스에 사용자 데이터를 저장해야 합니다.

세션 데이터 저장 솔루션으로 Upstash Redis가 탁월한 선택인 이유는 다음과 같습니다:

  • Redis와 마찬가지로 세션은 본질적으로 key:value 데이터 구조를 가집니다. 여기서 key는 세션 ID이고, value는 직렬화된 세션 데이터입니다.
  • Redis에는 만료(expiry) 메커니즘이 내장되어 있어, 만료된 세션을 별도로 정리하는 작업 부담을 크게 줄여줍니다.
  • 세션에는 민감한 사용자 데이터가 포함될 수 있는데, Upstash Redis는 저장되는 모든 데이터를 암호화합니다.
  • Upstash는 간단한 HTTP REST API를 사용합니다. HTTP는 서버리스 인프라에서 통신하기 가장 쉬운 방식입니다.

Upstash를 세션 제공자로 사용하는 방법

이 글은 필자가 작성한 Redis Session Storage Using Upstash 예제를 기반으로 합니다. Remix 저장소를 클론하여 직접 실습해 보세요.

1단계 - Upstash API 키 발급받기

  • https://upstash.com/에 접속하여 새 계정을 생성합니다.
  • 새로운 Redis DB를 생성합니다.
  • UPSTASH_REDIS_REST_URLUPSTASH_REDIS_REST_TOKEN 값을 복사한 뒤, Remix 프로젝트 루트 디렉터리에 .env 파일을 만들어 저장합니다.
  • dotenv 패키지를 설치합니다 — $ npm install --save-dev dotenv. 이를 통해 방금 만든 .env 파일의 환경 변수를 주입할 수 있습니다.
  • package.json 파일을 열고 dev 스크립트를 remix dev에서 dotenv/config node_modules/.bin/remix dev로 변경합니다.

2단계 - core createSessionStorage 구현으로 Upstash 세션 구현체 만들기

Remix는 createSessionStorage를 사용해 자신만의 세션 통합을 구축할 수 있는 훌륭한 API를 제공합니다. 이제 이 함수를 구현하여 Upstash를 연동해 보겠습니다.

// sessions/upstash.server.ts
import * as crypto from "crypto";
import { createSessionStorage } from "remix";

const upstashRedisRestUrl = process.env.UPSTASH_REDIS_REST_URL;

const headers = {
  Authorization: `Bearer ${process.env.UPSTASH_REDIS_REST_TOKEN}`,
  Accept: "application/json",
  "Content-Type": "application/json",
};

const expiresToSeconds = (expires) => {
  const now = new Date();
  const expiresDate = new Date(expires);
  const secondsDelta = expiresDate.getSeconds() - now.getSeconds();
  return secondsDelta < 0 ? 0 : secondsDelta;
};

// 자세한 내용은 https://remix.run/docs/en/v1/api/remix#createsessionstorage 참고
export function createUpstashSessionStorage({ cookie }: any) {
  return createSessionStorage({
    cookie,
    async createData(data, expires) {
      // 랜덤 ID 생성 - Remix의 core `createFileSessionStorage` 함수에서 가져온 방식
      const randomBytes = crypto.randomBytes(8);
      const id = Buffer.from(randomBytes).toString("hex");
      // Upstash Redis HTTP API 호출. 쿠키의 `expires` 속성에 따라 만료 시간 설정
      // `expiresToSeconds`를 사용해 날짜를 초 단위로 변환
      await fetch(
        `${upstashRedisRestUrl}/set/${id}?EX=${expiresToSeconds(expires)}`,
        {
          method: "post",
          body: JSON.stringify({ data }),
          headers,
        }
      );
      return id;
    },
    async readData(id) {
      const response = await fetch(`${upstashRedisRestUrl}/get/${id}`, {
        headers,
      });
      try {
        const { result } = await response.json();
        return JSON.parse(result).data;
      } catch (error) {
        return null;
      }
    },
    async updateData(id, data, expires) {
      await fetch(
        `${upstashRedisRestUrl}/set/${id}?EX=${expiresToSeconds(expires)}`,
        {
          method: "post",
          body: JSON.stringify({ data }),
          headers,
        }
      );
    },
    async deleteData(id) {
      await fetch(`${upstashRedisRestUrl}/del/${id}`, {
        method: "post",
        headers,
      });
    },
  });
}

방금 작성한 코드를 살펴보겠습니다.
라는 이름의 파일을 생성했으며, 이 파일은 createUpstashSessionStorage라는 함수를 export합니다.
이 함수는 cookie(뒤에서 자세히 설명)를 매개변수로 받고, Remix의 core createSessionStorage 팩토리 함수를 활용해 새로운 세션 제공자를 구현합니다.

함수 내부에서는 새 세션 생성(createData), 세션 값 읽기(readData), 세션 값 업데이트(updateData), 세션 삭제(deleteData)라는 createSessionStorage 프로토콜을 구현했습니다.

각 함수는 Upstash의 REST API를 통해 Redis 데이터베이스와 상호작용합니다.

주의할 점

  • 전달된 쿠키에는 js Date 형식의 쿠키 만료 날짜가 담겨 있습니다. Redis는 초 단위를 기대하기 때문에 expiresToSeconds 함수를 사용해 날짜를 초 단위로 변환합니다.
  • 쿠키를 설정할 때 만료 날짜 설정을 잊지 마세요. Redis가 만료된 세션을 자동으로 삭제해 줍니다.
  • 고유한 세션 ID를 생성하기 위해 crypto 모듈을 사용했습니다. 고유 ID를 만드는 다른 방법도 있지만, core createFileSessionStorage 함수에서 사용하는 것과 동일한 방식이라 이 옵션을 선택했습니다.

3단계 - 앱에서 createSessionStorage 사용하기

이제 자체 세션 스토리지 구현체를 만들었으니, 실제로 사용하는 방법을 살펴보겠습니다.

참고로 이후부터는 Upstash에 특화된 내용이 없습니다. 모든 로직은 sessions/upstash.server.ts 파일 안에 캡슐화되어 있습니다.

// sessions.server.ts
import { createCookie } from "remix";
import { createUpstashSessionStorage } from "~/sessions/upstash.server";

// 세션의 유효 기간을 설정합니다.
// 예시에서는 기능을 쉽게 확인할 수 있도록 매우 짧은 시간을 사용합니다.
const EXPIRATION_DURATION_IN_SECONDS = 10;

const expires = new Date();
expires.setSeconds(expires.getSeconds() + EXPIRATION_DURATION_IN_SECONDS);

const sessionCookie = createCookie("__session", {
  secrets: ["r3m1xr0ck1"],
  sameSite: true,
  expires,
});

const { getSession, commitSession, destroySession } =
  createUpstashSessionStorage({ cookie: sessionCookie });

export { getSession, commitSession, destroySession };

sessions.server.ts라는 파일을 생성하고 위 코드를 붙여넣습니다.
이 파일은 getSession, commitSession, destroySession 세 가지 함수를 export합니다. 이 함수들을 통해 앱이 세션과 상호작용할 수 있습니다. 또한 클라이언트 측에 세션 참조를 저장할 쿠키도 생성했습니다.

만료 시간은 비즈니스 요구 사항에 맞게 설정하세요. 자세한 내용은 MDN 쿠키 문서를 참고하세요.

Remix 라우트에서 세션 사용하기

Remix에서는 라우트별로 세션 사용을 정의할 수 있습니다. 다음 예제에서는 routes/index.tsx에서 session을 사용합니다. 이 예제는 세션 API 사용법만 보여줄 뿐, 특정 비즈니스 로직과 연결하는 내용은 이 글의 범위를 벗어납니다.

세션을 인증에 활용하는 예제가 필요하다면 https://github.com/remix-run/remix/tree/main/examples/remix-auth-form을 확인하세요.

// routes/index.tsx
import type { LoaderFunction } from "remix";
import { json, useLoaderData } from "remix";
import { commitSession, getSession } from "~/sessions.server";

export const loader: LoaderFunction = async ({ request }) => {
  // 쿠키에서 세션 가져오기
  const session = await getSession(request.headers.get("Cookie"));
  const myStoredData = session.get("myStoredData");
  // 세션이 없거나(생성된 적 없거나 만료된 경우) 새 세션 생성
  if (!myStoredData) {
    session.set("myStoredData", "Some data");
    return json(
      {
        message: "Created new session",
      },
      {
        headers: {
          "Set-Cookie": await commitSession(session),
        },
      }
    );
  }
  // 유효한 세션이 있다면 세션 정보 표시
  return json({
    message: `Showing Session info: ${myStoredData}`,
  });
};

export default function () {
  const data = useLoaderData();
  return <div>{data.message}</div>;
}

이 예제는 사용자 세션의 두 가지 가능한 상태(세션이 있는 경우와 없는 경우)를 처리하는 방법을 보여줍니다. 세션이 없는 사용자가 앱의 인덱스 페이지에 접속하면 새 세션이 생성되고 더미 데이터가 저장됩니다. 반면 유효한(만료되지 않은) 세션을 가진 사용자에게는 세션 데이터가 표시됩니다.

4단계 - 배포

Upstash를 활용한 세션 구현을 마쳤다면, 이제 원하는 어떤 배포 전략이든 자유롭게 선택할 수 있습니다.

배포 시 Upstash 환경 변수를 설정하는 것을 잊지 마세요.

부록

로컬 개발에서는 createFileSessionStorage를, 스테이징/프로덕션에서는 createUpstashSessionStorage 사용하기

오프라인 상태에서도 개발할 수 있기를 원할 것입니다. 로컬 개발 환경에서는 현재 NODE_ENV 값을 감지하여 createUpstashSessionStoragecreateFileSessionStorage로 교체할 수 있습니다.

Upstash 구현이 정상 동작하는지 테스트한 후, sessions/upstash.server.ts 파일을 다음과 같이 수정합니다.

아래 코드를:

// from sessions/upstash.server.ts
const { getSession, commitSession, destroySession } =
  createUpstashSessionStorage({ cookie: sessionCookie });

다음 코드로 교체합니다:

// from sessions/upstash.server.ts
const { getSession, commitSession, destroySession } = (process.env.NODE_ENV === "development") ?
  createFileSessionStorage({ cookie: sessionCookie, dir: './sessions' }) :
  createUpstashSessionStorage({ cookie: sessionCookie });

이제 로컬에서 개발할 때는 Upstash를 호출하는 대신 로컬 파일 시스템을 사용하게 됩니다.

결론

이번 글에서는 Upstash Redis DB를 활용해 세션 스토리지 데이터를 호스팅하는 방법을 살펴보았습니다. Remix API는 세션 스토리지의 구체적인 구현을 매우 잘 캡슐화하고 있어, 연동 과정이 매우 간단합니다. 예제를 직접 실행해 보고 싶다면 Remix 소스 코드의 redis-upstash-session 예제를 확인해 보세요.

즐거운 코딩 되세요!