1. Edge 런타임


Edge 런타임은 일반적인 Node.js 서버 환경과 달리 Edge Network에서 실행 가능한 경량화된 런타임 환경입니다. Vercel과 같은 플랫폼에서 제공되며, 전 세계에 분산된 Edge Location 중 사용자와 물리적으로 가까운 위치에서 코드를 실행합니다. Edge 런타임은 다음과 같은 특징을 가집니다.

<aside>

  1. 장점

  2. 단점

2. 미들웨어와 JWT Decode


Next.js에서 미들웨어는 Edge 런타임 환경에서 실행됩니다. 이전 글에서 작성했듯이 Trablock은 미들웨어에서 사용자 인증 토큰을 검증하기 때문에 JWT 토큰을 복호화할 수단이 필요합니다. 이를 위해 jwt-decode, jsonwebtoken, jose와 같은 검증된 라이브러리를 테스트했습니다. jwt-decodejsonwebtoken의 경우 Edge 런타임에서 지원하는 Node.js API를 요구해 제외되었고, jose는 Edge 런타임과 호환되지만 필요한 기능에 비해 복잡하고 무거운 라이브러리입니다. 프로젝트에서는 JWT 토큰 복호화 기능을 제외한 나머지 기능을 필요로 하지 않았습니다. 따라서 필요한 기능을 직접 구현해 사용하기로 결정했습니다.

// T: 본문 제네릭 타입
type JwtPayload<T extends object = Record<string, never>> = T & {
  iss?: string;
  sub?: string;
  aud?: string[] | string;
  exp?: number;
  nbf?: number;
  iat?: number;
  jti?: string;
};
%%{init: {'themeCSS': '.label { font-size: 12px; }'}}%%
flowchart TD
    A[JWT 토큰] --> B{토큰 존재?}
    B -- Yes --> D{올바른 토큰 형식?}
    B -- No --> J[에러 반환]
    D -- Yes --> F{유효한 헤더?}
    D -- No --> J
    F -- Yes --> H[본문 복호화]
    F -- No --> J
    H --> I{유효한 본문?}
    I -- Yes --> K[본문 복호화]
    I -- No --> J

초기 구현한 로직은 JWT 토큰의 형식이 유효한지 검증하고 복호화된 본문을 반환하는 간단한 형식이었습니다. 여기에 프로젝트를 진행하며 코드를 간결화하기 위해 기능을 추가했습니다. 토큰 형식 검증과 함께 시간 검증을 사전에 처리해 결과를 반환하면 토큰 유효성 검증 전체를 선언적으로 처리할 수 있습니다. 따라서 다음과 같은 타입과 로직을 추가했습니다.

// 기존 JwtPayload 타입
type BaseJwtPayload<T extends object = Record<string, never>> = T & {
  iss?: string;
  sub?: string;
  aud?: string[] | string;
  exp?: number;
  nbf?: number;
  iat?: number;
  jti?: string;
};

// 확장된 JwtPayload 유효성 검증 타입
type JwtPayloadValidation = {
  isValid: boolean; // 토큰 구조 검증
  isNotActive: boolean; // nbf 검증
  isExpired: boolean; // exp 검증
  error: string | null; // 에러 메세지, isValid 연동
};

// 최종 JwtPayload 타입
type JwtPayload<T extends object = Record<string, never>> = BaseJwtPayload<T> & JwtPayloadValidation;
%%{init: {'themeCSS': '.label { font-size: 12px; }'}}%%
flowchart TD
    A["본문 복호화<br>{ isValid: true }"] --> B{"nbf 검사<br>(isNotActive?)"}
    A --> C{"exp 검사<br>(isExpired?)"}
    B -- 유효 --> D["{ isNotActive: false }"]
    B -- 만료 --> E["{ isNotActive: true }"]
    C -- 유효 --> F["{ isExpired: false }"]
    C -- 만료 --> G["{ isExpired: true }"]
    D --> H[JwtPayload 반환]
    E --> H
    F --> H
    G --> H
function jwtDecode<T extends object = Record<string, never>>(token: string): JwtPayload<T> {
  // 토큰이 없을 경우
  if (!token)
    return {
      isValid: false,
      isNotActive: false,
      isExpired: false,
      error: 'Token is empty.'
    } as JwtPayload<T>;

  try {
    // JWT 구조 검증
    const splittedTokenList = token.split('.');
    if (splittedTokenList.length !== 3)
      return {
        isValid: false,
        isNotActive: false,
        isExpired: false,
        error: 'Invalid token format.'
      } as JwtPayload<T>;

    // Header 검증
    try {
      const decodedHeader = JSON.parse(Buffer.from(splittedTokenList[0], 'base64').toString());
      if (!decodedHeader.alg)
        return {
          isValid: false,
          isNotActive: false,
          isExpired: false,
          error: 'Algorithm is missing in header.'
        } as JwtPayload<T>;
    } catch {
      return {
        isValid: false,
        isNotActive: false,
        isExpired: false,
        error: 'Invalid header format.'
      } as JwtPayload<T>;
    }

    // payload 디코딩
    const base64Payload = splittedTokenList[1];
    const payloadBuffer = Buffer.from(base64Payload, 'base64');
    const decodedPayload = JSON.parse(payloadBuffer.toString()) as BaseJwtPayload<T>;

    // 반환할 payload 기본값
    const payload: JwtPayload<T> = {
      ...decodedPayload,
      isValid: true,
      isNotActive: false,
      isExpired: false,
      error: null
    };

    // 시간 기반 유효성 검증
    const currentTime = Math.floor(Date.now() / 1000);

    // nbf 검사
    if (decodedPayload.nbf && currentTime < decodedPayload.nbf) payload.isNotActive = true;

    // exp 검사
    if (decodedPayload.exp && currentTime > decodedPayload.exp) payload.isExpired = true;

    // 완성된 payload 반환
    return payload;
  } catch (error) {
    return {
      isValid: false,
      error: error instanceof Error ? error.message : 'Token parsing failed.'
    } as JwtPayload<T>;
  }
}