[Next.js] iron-session으로 무상태 세션 구현하기

POLO·2024년 9월 16일

Next.js 프로젝트에서 next-auth를 사용해 인증과 세션 관리를 진행해왔습니다. next-auth는 소셜 로그인을 포함한 다양한 인증 방식을 지원하고 있어 매우 유용했지만 프로젝트를 진행하면서 중간중간 해결해야 할 문제들이 있었습니다. 커스터마이징 요구 사항을 처리할 때는 특히 더 많은 시간이 들었던 거 같습니다.

그래서 이번 프로젝트에서는 인증은 직접 or 타 라이브러리를 사용해 구현하고, 세션 관리만 iron-session을 사용하는 방향으로 전환해보기로 했습니다.

iron-session 설치

npm add iron-session

iron-session 세션 관리 방법

  1. 세션 옵션 정의
  2. 세션 데이터의 유형 정의
  3. 세션 데이터 변수 설정
  4. 브라우저 쿠키 스토리지에 데이터 저장 또는 읽기
    • session.save(): 세션 변수를 브라우저 쿠키 스토리지에 암호화된 문자열로 저장
    • session.destroy(): 브라우저 쿠키 스토리지에 저장된 쿠키 값을 빈 값으로 설정
    • 쿠키 브라우저 스토리지에서 암호화된 문자열을 해독하여 세션 변수 읽기

1. 세션 옵션 정의

필수 옵션 및 선택 옵션을 세션 옵션에 정의하기 위해 session.ts를 작성합니다.

// src/lib/session.ts
import { SessionOptions } from "iron-session";

export const sessionOptions: SessionOptions = {
  cookieName: "myapp_session",
  password: process.env.SESSION_PASSWORD as string,
  cookieOptions: {
    secure: process.env.NODE_ENV === "production", // HTTPS에서만 쿠키 전송 (production일 때만 true)
    httpOnly: true, // 클라이언트 JavaScript에서 쿠키 접근 차단
    sameSite: "lax", // 사용자가 외부 사이트의 링크를 클릭해 사이트에 들어올 때 쿠키가 전송됨
    maxAge: 2147483647, // 최대 유효기간(약 68년)
    path: "/", // 쿠키가 유효한 경로 (기본값: 사이트 전체에 대해 유효)
  },
};

  • cookieName(필수): 브라우저에 저장될 유니크한 쿠키의 이름
  • password(필수): 쿠키를 암호화하는 데 사용되는 개인 키.
    • 최소 32자 이상
    • 1Password 같은 툴을 사용해 비밀번호를 생성할 수 있음
    • 다음과 같이 객체 배열도 가능
      ex) [{id: 2, password: "..."}, {id: 1, password: "..."}]
      이 방식을 사용하면 비밀번호 회전 가능
  • cookieOptions(선택)

2. 세션 데이터 타입 정의

세션 데이터 타입을 정의합니다.

// src/lib/session.ts
import { SessionOptions } from "iron-session";

export const sessionOptions: SessionOptions = {
  ...
};

export interface SessionData {
  nickname: string;
  email: string;
  isLoggedIn: boolean;
}

export const defaultSession: SessionData = {
  nickname: "",
  email: "",
  isLoggedIn: false,
};

SessionData 인터페이스를 만든 후 저는 닉네임, 이메일, 로그인 여부 필드를 추가해주었습니다.
세션이 가지고 있어야 할 사용자 데이터를 정의해 주는 과정입니다.

3. API Route에서 세션 관리 API 설정

Next.js API Route에 세션을 가져오고,세션을 저장하는 API를 작성합니다.

// src\app\api\session\route.ts
import { defaultSession, SessionData, sessionOptions } from "@/lib/session";
import { getIronSession } from "iron-session";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { NextRequest } from "next/server";

export async function GET(request: NextRequest) {
  const session = await getIronSession<SessionData>(cookies(), sessionOptions);

  const action = new URL(request.url).searchParams.get("action");
  if (action === "logout") {
    session.destroy();
    return redirect("/");
  }
  if (session.isLoggedIn !== true) {
    return Response.json(defaultSession);
  }

  return Response.json(session);
}

export async function POST(request: NextRequest) {
  const session = await getIronSession<SessionData>(cookies(), sessionOptions);
  const userInfo = await request.json();
  session.email = userInfo.email;
  session.nickname = userInfo.nickname;
  session.isLoggedIn = true;
  await session.save();
  return Response.redirect(`${request.nextUrl.href}`, 303);
}

action 쿼리가 logout인 경우 session.destroy()를 사용해서 쿠키를 빈값으로 만들어주도록 했습니다.
isLoggedInfalse인 경우에는 defaultSession을, true인 경우에는 session 데이터를 반환하도록 했습니다.

API 호출 함수 작성

클라이언트 컴포넌트에서 사용할 API 호출 함수를 작성합니다.

// src\entities\api\fetchCookieFromSession.ts
const fetchCookieFromSession = async () => {
  const res = await fetch("/api/session", {
    method: "GET",
  });

  const data = await res.json();
  console.log(res, data);
  return data;
};
export default fetchCookieFromSession;
// src\entities\api\saveCookieToSession.ts
const saveCookieToSession = async (email: string, nickname: string) => {
  await fetch("/api/session", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ email, nickname }),
  });
};

export default saveCookieToSession;

세션 save(), get() 테스트

Form 컴포넌트에서 이메일 인증코드 확인 후 새로운 유저를 만들고,
쿠키에 세션에 저장하는 로직으로 작성했습니다.

  const onSubmit = async (data: { verificationCode: string }) => {
    try {
      setIsErrorVerifying(false);

      setIsVerifying(true);

      const isVerified = await verifyVerificationCode(
        email,
        data.verificationCode,
      );

      if (isVerified) {
        const res = await createNewUser(email, password, nickname, "local");

        if (res.success) {
          await saveCookieToSession(email, nickname);
          incrementStep();
        }
      } else {
        alert("인증 코드가 일치하지 않습니다. 다시 시도해 주세요.");
      }
    } catch (error) {
      setIsErrorVerifying(true);
    } finally {
      setIsVerifying(false);
    }
  };

쿠키 스토리지에 가보면 다음과 같이 암호화된 세션 데이터가 값으로 들어간 쿠키가 잘 생성됐습니다.

세션 데이터를 잘 가져오는지 테스트를 위해 홈으로 가기 버튼에 잠시 테스트를 해 봤습니다.

      <Button
        className="w-full"
        type="button"
        variant="outline"
        onClick={async () => {
          console.log(await fetchCookieFromSession());
        }}
      >
        홈으로 가기
      </Button>

참고 자료

0개의 댓글