728x90
반응형

우여곡절 끝에 완성시킨 messenger cloe 프로젝트를 netlify에 빌드 시키려던 나는 어떤 오류에 가로막혔다.

처음엔 netlify에 업로드 된 프로젝트가 많아서 그런 줄 알고 vercel에 재업로드를 시켰으나,

두둥! 같은 오류가 뜨는 것이 아니겠는가.

 



유형 오류: 경로 "app/api/auth/[...nextauth]/route.ts"가 Next.js 경로의 필수 유형과 일치하지 않습니다.
  "authOptions"는 유효한 경로 내보내기 필드가 아닙니다.

 

nextAuth의 route가 경로의 필수 유형과 일치하지 않는다는 아주 당황스러운 오류였다.


영상 주인은 빌드만 잘 되더만 왜 나에게 이런 시련이 주어지는지에 대해 눈물을 좔좔 뽑으며 영상 주인의 github를 긁어 내가 오탈자를 냈나? 아니면 지운 구문을 가지고 있었나? 하는 것을 눈 빠지게 쳐다보며 고민했지만 줄바꿈 외에는 아무런 문제도 없었다. 그러면 대체 왜!!! 이런 오류가 발생하여 나를 괴롭게 한단 말인가.

하지만 여기서 포기하면 집념의 개발자라는 이름을 자소서에 함부로 쓰지 못할 터.
완성하기까지 걸린 시간이 아까워서라도 꼭 빌드 시키고야 말겠다는 의지에 불타올라 혼자 삼일 밤낮으로 고민하다가 지친 나는 gtp의 도움을 살짝쿵 받기로 했다.

맨땅에 헤딩도 흙밭에 해야 살아있지 아스팔트 바닥에다 하면 머리만 깨지고 나만 죽는다.

 

GPT의 답변은 이랬다.


이 오류는 Next.js에서 특정 경로에 맞는 내보내기(export) 필드가 잘못 정의되었기 때문에 발생하는 것입니다. 이 문제는 Next.js 13 이상 버전에서 app 디렉토리와 관련된 라우트 파일에서 흔히 발생할 수 있습니다.

NextAuth의 설정을 위한 코드는 정상으로 보이지만, Next.js 13의 새로운 app 디렉토리 기반 라우팅 시스템에서는 경로 파일에 기본 내보내기(default export)를 사용하는 것이 아니라 GET, POST와 같은 HTTP 메서드를 특정하여 내보내야 합니다.

문제점: route.ts 파일에서 authOptions를 내보내는 대신, NextAuth 핸들러를 GET과 POST로 내보내야 합니다. 이미 코드의 마지막에 handler를 GET과 POST로 내보내고 있기 때문에 authOptions를 따로 내보낼 필요가 없습니다.

authOptions가 경로 파일에서 직접 내보내지기 때문에 발생하는 오류입니다.



해결책:

authOptions를 내보내는 부분을 제거합니다.
이미 있는 handler를 GET과 POST로 내보내는 부분을 유지합니다.

 

 



GPT가 export와 몇 개의 구문을 제거한 코드를 알려줬기에 그대로 입력 후 git에 업로드 후 희망차게 다시 빌드했으나, 돌아온 것은....


또 다른 오류였다. 오 마이 갓김치. (난 파김치가 좋다.) 
어쨋든, 이번 오류는 authOptions/route.ts가 로컬에서 authOptions를 선언하긴 하지만 내보내진 않는다. 는 거였다.


그래서 GPT도 믿을 게 못 된다. 발로 뛰어보자 싶었던 나는 영상 주인의 discord 서버를 들어가게 된다.

이 무슨 행운인가. 나랑 같은 오류로 고생하는 사람을 만나게 된 것이다.
하늘에서 빛줄기가 내려와 나를 감싸는 기분이었다.

드디어 나도 이 오류를 아름답게 해결할 수 있을 것이라는 희망에 부푼 가슴을 안고 열어봤더니


코드를 새로 주셨는지 방법을 알려주셨는진 모르겠지만 (당사자끼리의 대화다보니) 코드 어딘가가 바뀌었고, 그곳에서 새로운 오류가 발생 중이라는 얘기 중이었다.

문맥 상으로는 route.ts를 libs/auth.ts로 변경하는 코드를 새로 작성하셨는데 그럼에도 불구하고 빌드 과정에서 오류가 있으셨던 모양.

근데 댓글을 달아주신 분은 어댑터에 문자열 유형을 지정해야 한다고 하셨고, 이 댓글을 본 작성자분은 코드를 보여줄 수 있냐고 여쭤보셨다.

또 그걸 본 댓글 다신 분이 나는 코드를 보여줬다고 8월 24일에 마지막 댓글을 남기신 것이 이 글의 끝이었다.
이게 한국 블로그에만 있는 비밀 댓글입니다. 하는 그런 건가? 
서양판 비밀 댓글입니다. 이런 건가? 싶었지만 해결책의 실마리라도 잡은 나는 이 얇은 실을 붙잡고 오류의 미궁을 헤쳐나가 보기로 했다.

원래 이런 건 혼자 맨 땅에 헤딩도 해보고, 삽질도 해 보고, 티스푼 같은 얇고 가는 숟가락으로 땅도 파 봐야 기억에 남고 마음에 남아 비슷한 일을 당했을 때 혼자서 해결하는 멋진 어른이로 자라는 법이다.

 

그래서 시키는 대로 libs 폴더에 nextAuthOptions.ts 라는 파일을 만든 후 원래의 route.ts 파일을 싹 다 긁어서 붙여넣은 후 이제 됐나? 하고 바로 다시 build를 시켰더니 이번에는 

authOptions의 import 경로가 잘못 되었다는 오류가 발생했다.

그렇다. 겨우 그정도로 장땡이 아니었던 것이다.

 

authOptions가 import된 모든 파일의 경로를 nextauth/route.ts에서 libs/nextAuthOptions로 변경 시키는 작업이 필요했던 것.

 

 

이게 원래 nextauth/route.ts 파일이고,

import bcrypt from "bcrypt";
import NextAuth, { AuthOptions } from "next-auth"; // 이 부분 변경됨
import CredentialsProvider from "next-auth/providers/credentials";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";

import prisma from "@/app/libs/prismadb";

export const authOptions: AuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID as string,
      clientSecret: process.env.GITHUB_SECRET as string,
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    }),
    CredentialsProvider({
      name: 'credentials',
      credentials: {
        email: { label: 'email', type: 'text' },
        password: { label: 'password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error('Invalid Credentials');
        }

        const user = await prisma.user.findUnique({
          where: {
            email: credentials.email
          }
        });

        if (!user || !user?.hashedPassword) {
          throw new Error('Invalid credentials');
        }

        const isCorrectPassword = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        );

        if (!isCorrectPassword) {
          throw new Error('Invalid credentials');
        }

        return user;
      }
    })
  ],
  debug: process.env.NODE_ENV === 'development',
  session: {
    strategy: "jwt",
  },
  secret: process.env.NEXTAUTH_SECRET,
};

const handler = NextAuth(authOptions); // 여기와

export { handler as GET, handler as POST }; // 여기가 사라짐

 

 

이게 바뀐 libs/nextAuthOptions.ts 파일이다.

// lib/nextAuthOptions.ts
import bcrypt from "bcrypt";
import { AuthOptions } from "next-auth";	//이 부분
import CredentialsProvider from "next-auth/providers/credentials";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import prisma from "@/app/libs/prismadb";

export const authOptions: AuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID as string,
      clientSecret: process.env.GITHUB_SECRET as string,
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID as string,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
    }),
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: { label: "email", type: "text" },
        password: { label: "password", type: "password" },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          throw new Error("Invalid Credentials");
        }

        const user = await prisma.user.findUnique({
          where: { email: credentials.email },
        });

        if (!user || !user?.hashedPassword) {
          throw new Error("Invalid credentials");
        }

        const isCorrectPassword = await bcrypt.compare(
          credentials.password,
          user.hashedPassword
        );

        if (!isCorrectPassword) {
          throw new Error("Invalid credentials");
        }

        return user;
      },
    }),
  ],
  debug: process.env.NODE_ENV === "development",
  session: { strategy: "jwt" },
  secret: process.env.NEXTAUTH_SECRET,
};

import의 NextAuth 구문이 사라지고, const hnadler = NextAuth(authOptions)와 export 구문이 사라졌다.

 

두 코드의 차이점은

1. 파일 위치 및 import

route.ts는 직접 NextAuth를 설정하고 핸들러를 내보내는 코드지만, nextAuthOptions은 anthOptions를 정의하는 설정 파일이며, 이 설정 파일은 NextAuth 인스턴스를 생성하는 메인 파일에서 import하여 사용할 수 있다는 차이점이 있다.

 

2. 코드 구조

route.ts는 authOoptions를 직접 정의하고 NextAuth 핸들러를 생성하고 export { handler as GET, handler as POST }로 핸들러를 내보낸다. 그러나 nextAuthOptions는 authOptions를 정의하게 되는데 이 authOptions는 libs/nextAuthOptions.ts에서 정의되며, NextAuth 핸들러는 이 파일을 import 하여 사용하는 다른 파일에서 생성되게 될 것이다.

 

3. 모듈화

route.ts는 authOptios와 NextAut 핸들러를 한 파일에서 정의하고 내보기에 이 파일이 NextAuth 관련 모든 기능을 포함하고 있지만, nextAuthOptions는 authOptions만 정의하고, 핸들러는 다른 파일에서 생성한다. 이로 인해 설정화 핸들러가 분리되며, 설정 파일과 핸들러를 분리하면 코드의 유지 보수성과 가독성이 높아진다.

 

정도의 차이점이 있다.

다른 건 그렇다 치더라도, 긴 코드가 줄어들어 가독성이 좋아진 건 알겠지만 코드의 유지 보수성은 체감이 잘 안 된다.

하나였던 파일이 세 개로 늘어나기 때문이다.

 

nextAuthOptions말고도 nextAuthOptions를 import해서 사용하는 다른 파일들 역시 경로를 바꿔주어야 한다.

 

getSession은  원래 authOptions이 api/auth/[...nextAuth]/route에서 import 되었는데, 

import { getServerSession } from "next-auth";

import { authOptions } from "../api/auth/[...nextauth]/route"; //route에서 import 되던 것

export default async function getSession() {
  return await getServerSession(authOptions);
}

 

이런 식으로 libs/nextAuthOptions에서 import 되도록 변경해주어야 하고,

import { getServerSession } from "next-auth";
import { authOptions } from "@/app/libs/nextAuthOptions"; 	//route에서 import 되던 게 libs로 변경됨

export default async function getSession() {
  return await getServerSession(authOptions);
}

 

 

 

 

authOptions을 import 해뒀던 pages/api/pusher 폴더에 있던 auth 파일 역시

import { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";

import { pusherServer } from "@/app/libs/pusher";
import { authOptions } from "@/app/api/auth/[...nextauth]/route"; // 이 부분

export default async function handler(
  request: NextApiRequest,
  response: NextApiResponse
) {
  const session = await getServerSession(request, response, authOptions);

  if (!session?.user?.email) {
    return response.status(401);
  }

  const socketId = request.body.socket_id;
  const channel = request.body.channel_name;
  const data = {
    user_id: session.user.email
  };

  const authResponse = pusherServer.authorizeChannel(socketId, channel, data);

  return response.send(authResponse);
}

 

api/auth/[...nextauth]/route에서 import 하던 authOptions을 

 

import { NextApiRequest, NextApiResponse } from "next";
import { getServerSession } from "next-auth";

import { pusherServer } from "@/app/libs/pusher";
import { authOptions } from "@/app/libs/nextAuthOptions"; // 이 부분

export default async function handler(
  request: NextApiRequest,
  response: NextApiResponse
) {
  const session = await getServerSession(request, response, authOptions);

  if (!session?.user?.email) {
    return response.status(401);
  }

  const socketId = request.body.socket_id;
  const channel = request.body.channel_name;
  const data = {
    user_id: session.user.email,
  };

  const authResponse = pusherServer.authorizeChannel(socketId, channel, data);

  return response.send(authResponse);
}

 

 

이런 식으로 libs/nextAuthOptions 에서 import 하도록 변경해준 후, 변경된 파일을 다시 git에 업로드 해주면,

아름답게 vercel에 build 할 수 있게 되는 것이다.

 

짜잔 

보이시는가?

영롱한 vercel이 적힌 도메인의 자태가.

드디어 해결을 본 것이다.

 

고생한 거에 비해 생각보다 별 거 아닌 문제였어서 김이 좀 샜지만, 다음 번에도 비슷한 문제가 생기면 그 때는 허둥대지 않고 침착하게 차근차근 해결하는 멋진 개발자 어른이가 되길 바라며, 

나와 같은 고생을 하고 있을지도 모르는 미래의 개발자 동료들에게 도움이 되길 바라며 일지(를 가장한 하소연)을 마치겠다.

 

 

+ Recent posts