[Next.js] Middleware로 로그인 여부 확인 중복 코드 줄이기

배준형·2024년 2월 19일
2

서문

안녕하세요. 인재 채용 플랫폼 잡다의 웹 프론트엔드 개발을 담당하고 있는 배준형입니다.

최근 잡다 매칭 서비스와 관련된 백오피스 개발을 시작했는데요. 로그인 여부에 따른 페이지 접근 제어 기능 구현이 필요했습니다. 로그인 여부를 확인하고, 로그인하지 않은 사용자는 로그인 페이지로 리다이렉트 처리하는 작업이 전체 페이지에 걸쳐 반복적으로 발생할 수 있는 상황이었습니다.

이를 해결하기 위해 Next.JS Middleware 기능을 활용했는데요. 중복 코드를 줄이고 효율적인 로그인 여부 확인 및 리다이렉트 처리를 구현할 수 있었습니다. 관련하여 알게 된 내용을 공유하고자 합니다.


Next.js 미들웨어

Next.js 미들웨어는 서버 측에서 요청을 처리하기 전에 코드를 실행할 수 있는 기능입니다. 이를 통해 다음과 같은 다양한 작업을 수행할 수 있습니다.

  • 요청 검증: 요청 헤더, 쿠키, URL 등을 검사하여 유효성을 확인하고 필요에 따라 권한을 부여하거나 거부할 수 있습니다.
  • 헤더 수정: 응답 헤더에 추가 정보를 추가하거나 기존 정보를 수정하여 클라이언트에 전달할 수 있습니다.
  • 응답 변경: 요청에 따라 응답 내용을 직접 생성하거나 다른 페이지로 리다이렉트할 수 있습니다.

Next.js 미들웨어는 Next.js 12.2 버전부터 안정화되었는데요. 요청 처리 과정에서 다음과 같은 순서대로 실행됩니다.

  1. next.config.js에서 설정된 headers
  2. next.config.js에서 설정된 redirects
  3. middleware: 페이지 리라이트, 리다이렉트 등을 수행합니다.
  4. next.config.js에서 설정된 beforeFiles (페이지 리라이트)
  5. 파일 시스템 기반 라우팅: public/_next/static/pages/app/ 등
  6. next.config.js에서 설정된 afterFiles (페이지 리라이트)
  7. 동적 라우팅: /blog/[slug]
  8. next.config.js에서 설정된 fallback (페이지 리라이트)

next.config.js에서 설정된 항목들과 미들웨어는 모두 위 순서대로 실행되는데, 미들웨어를 설정하고 항상 실행되는 것이 싫다면 특정 경로에서만 실행하도록 설정할 수도 있습니다. 이를 위해서는 config matcher를 사용하여 원하는 경로를 지정하면 됩니다.


Middleware의 활용

앞서 살펴본 미들웨어는 어떤 방식으로 활용할 수 있을까요? 로그인 여부 확인, 리다이렉트 처리, 라우팅 제어, 로깅 등 다양한 용도로 활용할 수 있으며, 중복 코드를 줄이고 개발 효율성을 높일 수 있습니다.


쿠키 조회
next/server 패키지에서 NextRequestNextResponse 타입을 불러와 요청과 응답 객체를 생성합니다. 다음으로, request.cookies 객체를 사용하여 쿠키에 접근하고 특정 쿠키 값을 확인하거나 모든 쿠키를 가져올 수 있습니다.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  let cookie = request.cookies.get('nextjs')
  console.log(cookie) // => { name: 'nextjs', value: 'fast', Path: '/' }
  const allCookies = request.cookies.getAll()
  console.log(allCookies) // => [{ name: 'nextjs', value: 'fast' }]
 
  request.cookies.has('nextjs') // => true
  request.cookies.delete('nextjs')
  request.cookies.has('nextjs') // => false
 
  // Setting cookies on the response using the `ResponseCookies` API
  const response = NextResponse.next()
  response.cookies.set('vercel', 'fast')
  response.cookies.set({
    name: 'vercel',
    value: 'fast',
    path: '/',
  })
  cookie = response.cookies.get('vercel')
  console.log(cookie) // => { name: 'vercel', value: 'fast', Path: '/' }
  // The outgoing response will have a `Set-Cookie:vercel=fast;path=/` header.
 
  return response
}

이 코드는 NextJS 미들웨어 공식 홈페이지에서 확인할 수 있는 코드인데요. Request, Response 모두에서 쿠키를 관리할 수 있습니다.


Routing 제어

import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  return NextResponse.redirect('/new-url');
}

쿠키에 접근하는 것 외에도 Routing 제어도 가능합니다. Cookies를 확인해서 인증을 거치고, 특정 권한을 갖고 있지 않은 유저를 리다이렉션 처리할 수 있겠죠.


Logging

import { NextResponse, type NextRequest } from "next/server";

export default function middleware(request: NextRequest) {
  const { nextUrl, method, headers } = request;
  const { pathname } = nextUrl;

  console.log(`[${method}] ${pathname}`);
  console.log(headers);

  return NextResponse.next();
}

각 요청에 대해 요청 정보를 추출하여 콘솔에 정보를 출력할 수도 있습니다. 이를 통해 디버깅, 에러 추적 등에 활용할 수 있을 것 같아요.


Middleware 없이 로그인 여부 확인하기

미들웨어는 Next.js에서 강력한 기능을 제공하지만, 모든 상황에 적합한 것은 아닙니다. 로그인 여부 확인과 리다이렉트 처리를 미들웨어 없이 구현하는 방법도 존재하며, 미들웨어를 사용하지 않는 것이 상황에 따라 더 적합할 수 있습니다. 미들웨어를 적용한 것과 적용하지 않은 것의 차이를 알아보기 위해 미들웨어를 사용하지 않고 로그인 여부를 확인하는 방법에 대해 먼저 알아보겠습니다.

우선, 백오피스 로그인 흐름은 다음과 같은데요.

  1. JWT 형태 로그인 구현
  2. 로그인 성공 시, 만료 시간과 accessToken을 쿠키에 저장
  3. 쿠키에 accessToken 존재 여부로 로그인 여부 판단
  4. accessToken 만료 시 쿠키에서 제거
  5. 변질된 accessToken은 API 응답 에러 처리

이를 각 Page에서 확인해야 합니다. 미들웨어 없이 로그인 여부를 확인하는 방법을 먼저 알아보면 아래와 같은 방법들이 있을 것 같아요.


useEffect로 리다이렉트

페이지 컴포넌트에서 직접 로그인 여부 확인 방법 중 하나로 useEffect Hook을 사용하여 컴포넌트 마운트 시 쿠키에 accessToken 존재 여부를 확인하고 리다이렉트 처리하는 방법을 살펴보겠습니다.

예시 코드

import { useEffect } from "react";
import { useCookies } from "react-cookie";
import { useRouter } from "next/router";

const ProtectedPage = () => {
  const [cookies] = useCookies();
  const router = useRouter();

  useEffect(() => {
    const accessToken = cookies.get("accessToken");

    if (!accessToken) {
      router.push("/login");
    }
  }, []);

  return (
    <div>
      <h1>보호된 페이지</h1>
      <p>이 페이지는 로그인된 사용자만 접근할 수 있습니다.</p>
    </div>
  );
};

export default ProtectedPage;

장점

  • 구현이 간단하고 직관적입니다.
  • 로그인 여부 확인 로직을 컴포넌트 외부로 분리할 수 있습니다.

단점

  • 모든 페이지 컴포넌트에 동일한 코드 반복해야 합니다.

getStaticProps 또는 getServerSideProps

페이지 컴포넌트에서 직접 로그인 여부를 확인하는 방법 외에, getStaticProps 또는 getServerSideProps 함수를 사용하여 로그인 여부를 확인하고 리다이렉트 처리하는 방법도 존재합니다.

예시 코드

const ProtectedPage = () => {
  // ...
}

export async function getStaticProps(context: GetStaticPropsContext) {
  const { cookies } = context.req;
  const accessToken = cookies.get(ACCESS_TOKEN_KEY);

  if (!accessToken) {
    return {
      redirect: {
        destination: "/login",
        permanent: false,
      },
    };
  }

  // ...

  return {
    props: {
      // ...
    },
  };
}

장점

  • 페이지 로딩 시점에 로그인 여부를 확인하여 성능이 향상됩니다.
  • SEO (Search Engine Optimization)에 유리합니다.

단점

  • getStaticProps는 빌드 타임에 실행되므로 페이지 로딩 속도에 영향을 줄 수 있습니다.
  • getServerSideProps는 매 요청마다 실행되므로 서버 부하가 증가할 수 있습니다.

HOC로 리다이렉트

HOC (Higher-Order Component)는 컴포넌트를 반환하는 컴포넌트로 공통 기능을 HOC에서 처리하여 중복 코드를 줄일 수 있습니다.

예시 코드

import { useEffect } from "react";
import { useRouter } from "next/router";
import { useCookies } from "react-cookie";

const withAuth =
  <P,>(WrappedComponent: React.ComponentType<P>) =>
  (props: React.PropsWithChildren<P>) => {
    const [cookies] = useCookies();
    const router = useRouter();

    useEffect(() => {
      const accessToken = cookies.get("accessToken");

      if (!accessToken) {
        router.push("/login");
      }
    }, []);

    return <WrappedComponent {...props} />;
  };

export default withAuth;

// 사용하는 곳에서 HOC로 컴포넌트를 감싸 사용합니다.
const ProtectedPage() {
  return (
    <div>
      <AuthMyPage />
    </div>
  );
}

export default withAuth(ProtectedPage);

장점:

  • 코드 재사용성이 높습니다.
  • 코드 관리 및 유지보수 용이합니다.

단점:

  • 코드 복잡도 증가의 가능성이 있습니다.
    • 만약 withAuth 외에도 여러 HOC가 만들어진다면 코드가 복잡해집니다.
    • ex) export default withAuth(withStore(withSomeLogic(Page))));

문제점

위 코드들은 직관적이고 간결합니다. 위 경우 말고도 여러가지 방법이 있을 것 같아요.

그런데, 언급한 방법 모두 로그인이 되어 있어야 하는 페이지가 많아질수록 중복 코드가 많아지게 됩니다. 리다이렉트 하는 코드만 분리할 수 있다고 하더라도 page 파일마다 작성해 줘야 하기에 완전히 중복을 제거할 수도 없고요.


Middleware로 로그인 여부 확인하기

이 경우 미들웨어를 활용하면 중복 코드를 최대한 줄이고, 한 곳에서 리다이렉트 처리를 컨트롤할 수 있습니다. 미들웨어는 middleware.ts(or .js) 파일을 pages, app, src 등의 디렉토리에 만들어 사용하면 되는데요.

예시 코드

// middleware.ts
import { NextResponse, type NextRequest } from "next/server";

const AUTH_PAGES = ["/", "/login"];

export default function middleware(request: NextRequest) {
  const { nextUrl, cookies } = request;
  const { origin, pathname } = nextUrl;
  const accessToken = cookies.get(ACCESS_TOKEN_KEY);

  // 로그인이 필요 없는 페이지
  if (AUTH_PAGES.some((page) => pathname.startsWith(page))) {
    // 로그인 되어 있는 경우 메인 페이지로 리다이렉트
    if (accessToken) {
      return NextResponse.redirect(MAIN_PAGE);
    } else {
      // 로그인이 필요 없는 페이지는 그냥 다음 요청으로 진행
      return NextResponse.next();
    }
  }

  // 로그인이 필요한 페이지
  if (!accessToken) {
    // 로그인 페이지로 리다이렉트
    return NextResponse.redirect(LOGIN_PAGE);
  }

  // 로그인 되어 있는 경우 요청 페이지로 진행
  return NextResponse.next();
}
  1. AUTH_PAGES 배열에 로그인이 필요 없는 페이지 경로를 설정합니다.
  2. accessToken 값을 쿠키에서 가져옵니다.
  3. 로그인이 필요 없는 페이지인 경우:
    • 로그인 되어 있는 경우 메인 페이지로 리다이렉트합니다.
    • 로그인이 되어 있지 않은 경우 그냥 다음 요청으로 진행합니다.
  4. 로그인이 필요한 페이지인 경우:
    • 로그인 되어 있지 않은 경우 로그인 페이지로 리다이렉트합니다.
    • 로그인 되어 있는 경우 요청 페이지로 진행합니다.

/, /login 페이지에선 로그인 창을 보여주고, 그 외의 페이지에선 로그인이 되어 있는 상태여야 합니다. 그 과정을 미들웨어로 처리한 것인데, 각 페이지에서 로그인 여부를 확인하고 리다이렉트 시키는 과정을 미들웨어 하나로 해결했습니다. 이제 page 작업에 로그인 여부를 확인할 필요 없이 필요한 동작만 처리하면 되겠네요.


정리

Next js Middleware를 이용해 Cookie, Routing 제어, Logging 등 다양하게 활용할 수 있는데요. 저는 /, /login 두 페이지 외에 전체 페이지에 로그인 여부를 확인하고 리다이렉트 하는 기능에 활용했습니다. 결과적으로 중복되는 작업을 완전히 줄였고, 이후 작업에선 로그인 여부를 확인하지 않아도 돼서 조금이나마 편해졌습니다. 예시 코드의 경우 accessToken 값을 확인하고 없다면 리다이렉트 하는 코드만 존재하는데요. 이를 잘 활용하여 Refresh Token을 재발급 하는데도 사용할 수 있습니다.

제 경우엔 로그인 페이지로그인 페이지 외 모든 페이지 2가지로 나뉘기에 미들웨어 사용이 적절했던 것 같아요. 그런데 미들웨어가 항상 좋은 것은 아닙니다. 모든 페이지에 로그인 여부 확인 로직이 필요한 경우가 아니라면 불필요한 오버헤드가 발생할 수 있습니다.

그런 경우엔 미들웨어 대신 useEffectgetServerSideProps 같은 기능을 활용하여 필요한 곳에서만 처리를 하는 것이 더 좋은 방법이 되겠죠. 따라서 프로젝트 규모나 코드 관리 방식, 개발자 경험 등을 고려해서 적절히 선택하면 좋을 것 같습니다.


참조

profile
프론트엔드 개발자 배준형입니다.

1개의 댓글

comment-user-thumbnail
2024년 8월 12일

와우 너무 좋은 자료인 것 같습니다 :)

답글 달기