[코드잇] 위클리 미션 수행기 (14주차)

woolee의 기록보관소·2023년 6월 20일

코드잇부트캠프0기

목록 보기
20/24

next-auth로 credentials 인증/인가 처리하기 (prisma/mongodb)

prisma 패키지를 설치하고

npm install -D prisma 

새로운 prisma 프로젝트를 시작한다.

npx prisma init 

prisma/schema.prisma에서 provider를 mongodb로 변경해준다.

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

prisma/client, adapter, next-auth도 설치해준다.

npm install next-auth @prisma/client @next-auth/prisma-adapter 

Nextjs는 기본적으로 우리 어플리케이션을 hot reload한다.
이때마다 계속해서 prisma client가 계속생성되는데,
이걸 막기 위해 prisma 인스턴스를 미리 생성해두고 재사용해야 한다.

// lib/prismadb.ts

import { PrismaClient } from "@prisma/client";

declare global {
  // eslint-disable-next-line no-var
  var prisma: PrismaClient | undefined;
}

const client = globalThis.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalThis.prisma = client;
}

export default client;

prisma 스키마는 다음과 같이 작성하고

MongoDB

// prisma/schema.prisma

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mongodb"
  url      = env("DATABASE_URL")
}

model User {
  id String @id @default(auto()) @map("_id") @db.ObjectId 
  name String
  image String? 
  email String? @unique 
  emailVerified DateTime? 
  hashedPassword String? 
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  favoriteIds String[] @db.ObjectId
  sessions Session[] 
  accounts Account[] 
}

model Account {
  id String @id @default(auto()) @map("_id") @db.ObjectId
  userId String @db.ObjectId 
  type String 
  provider String 
  providerAccountId String 
  refresh_token String? @db.String 
  access_token String? @db.String
  expires_at Int? 
  token_type String? 
  scope String? 
  id_token String? @db.String 
  session_state String? 

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([provider, providerAccountId])
}

model Session {
  id String @id @default(auto()) @map("_id") @db.ObjectId
  sessionToken String @unique
  userId String @db.ObjectId 
  expires DateTime

  user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model VerificationToken {
  id String @id @default(auto()) @map("_id") @db.ObjectId
  identifier String 
  token String @unique
  expires DateTime

  @@unique([identifier, token])
}

mongodb에 push 해준다.

npx prisma db push

nextauth는 다음과 같이 작성해준다.

// app/api/auth/[...nextauth].route.ts 

import prisma from "@/lib/prismadb";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import bcrypt from "bcryptjs";
import { NextAuthOptions } from "next-auth";
import NextAuth from "next-auth/next";
import Credentials from "next-auth/providers/credentials";

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma),
  providers: [
    Credentials({
      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) {
          // hashedPassword가 없으면 OAuth 로그인한 유저
          throw new Error("Invalid credentials");
        }

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

        if (!isCorrectPassword) {
          throw new Error("Invalid credentials");
        }
        console.log(user);
        return user;
      },
    }),
  ],
  session: {
    strategy: "jwt",
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  pages: {
    signIn: "api/auth/login",
  },
  callbacks: {
    async jwt({ token, user }) {
      return { ...token, ...user };
    },
    async session({ session, token }) {
      session.user = token;
      return session;
    },
  },
};

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

로그인 페이지와 회원가입 페이지는 다음과 같이 임시로 작성해주었다.

// app/api/auth/login/Login.tsx 

"use client";

import { useRef } from "react";

import { signIn } from "next-auth/react";

const Login = () => {
  const email = useRef<HTMLInputElement>(null);
  const password = useRef<HTMLInputElement>(null);

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (!email.current || !password.current) {
      return;
    }

    try {
      // 로그인 페이지는 signIn 함수를 사용하면 된다.
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const data = await signIn("credentials", {
        email: email.current.value,
        password: password.current.value,
      });
    } catch (error) {
      console.log(error);
    }
  };
  return (
    <form onSubmit={onSubmit}>
      <input type="text" ref={email} />
      <input type="password" ref={password} />
      <button>로그인</button>
    </form>
  );
};

export default Login;
// app/api/auth/register/Register.tsx 

"use client";

import { useRef } from "react";

import { useRouter } from "next/navigation";

import axios from "axios";

const Register = () => {
  const name = useRef<HTMLInputElement>(null);
  const email = useRef<HTMLInputElement>(null);
  const password = useRef<HTMLInputElement>(null);
  const router = useRouter();

  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (!name.current || !email.current || !password.current) {
      return;
    }

    try {
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      const { data } = await axios.post("/api/register", {
        name: name.current.value,
        email: email.current.value,
        password: password.current.value,
      });
      router.push("/api/auth/login");
    } catch (error) {
      console.log(error);
    }
  };

  return (
    <form onSubmit={onSubmit}>
      <input type="text" ref={name} />
      <input type="text" ref={email} />
      <input type="password" ref={password} />
      <button>등록하기</button>
    </form>
  );
};

export default Register;

미들웨어를 작성해 접근 권한을 부여했다.

미들웨어를 루트 경로에 작성하면 에러가 발생할 수 있는데, 버전 업데이트해주면 된다.

Module parse failed: Identifier 'NextResponse' has already been declared (3:6) Middleware.ts(NextJs)

// middleware.ts

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

import { getToken } from "next-auth/jwt";

export { default } from "next-auth/middleware";

export async function middleware(req: NextRequest) {
  // getToken을 사용하면 session 데이터 가져올 수 있다.
  // 원래 jwt 토큰 생성할 때 secret 토큰을 넣어줬었다.
  const session = await getToken({ req, secret: process.env.JWT_SECRET });
  const pathname = req.nextUrl.pathname;

  // 로그인된 유저만 접근 가능
  // 경로는 /folder인데 session이 없다는 건 로그인되지 않았다는 의미
  if (pathname.startsWith("/folder") && !session) {
    return NextResponse.redirect(new URL("/api/auth/login", req.url));
  }

  // 로그인된 유저는 로그인, 회원가입 페이지에 접근 X
  if (pathname.startsWith("/api/auth") && session) {
    return NextResponse.redirect(new URL("/", req.url));
  }

  // 위 경우가 전부 아닐 경우 : 통과시켜줘서 원하는 url로 갈 수 있게 해준다.
  return NextResponse.next();
}

이제 회원가입을 하면 유저 정보가 mongodb에 저장되고 로그인도 할 수 있다.

이걸 어떻게 검증하냐면, 유저 데이터를 찾는 훅을 하나 만들어서 서버 컴포넌트 레이아웃 쪽에서 호출하면 된다.

// app/api/auth/login/Login.tsx 

import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import getUser from "@/utils/api/getUser";
import { tempUserDatas } from "@/utils/constants";
import { getServerSession } from "next-auth";

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

export default async function getCurrentUser() {
  try {
    const session = await getSession();

    if (!session?.user?.email) {
      return null;
    }

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

    if (!currentUser) {
      return null;
    }

    const userId = tempUserDatas[currentUser.id];

    const user = await getUser(userId);

    return user;
  } catch (error) {
    return null;
  }
}

예를 들어 이런 식으로!

// app/shared/layout.tsx 

export default async function SharedLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const userProfile = await getCurrentUser();
  return (
    <>
      <Gnb user={userProfile} />
      {children}
      <Footer />
    </>
  );
}

axios 타입 작성하기

axios의 공통 인터페이스를 작성하고

// types/axiosInterface.ts 

/* eslint-disable @typescript-eslint/no-explicit-any */
import {
  AxiosInstance,
  AxiosInterceptorManager,
  AxiosResponse,
  InternalAxiosRequestConfig,
} from "axios";

type CustomAxiosResponse<T = any> = {
  response?: T;
  refreshToken?: string;
};

export interface CustomAxiosInterface extends AxiosInstance {
  interceptors: {
    request: AxiosInterceptorManager<InternalAxiosRequestConfig>;
    response: AxiosInterceptorManager<AxiosResponse<CustomAxiosResponse>>;
  };

  get<T>(url: string, config?: InternalAxiosRequestConfig): Promise<T>;
  delete<T>(url: string, config?: InternalAxiosRequestConfig): Promise<T>;
  post<T>(
    url: string,
    data?: any,
    config?: InternalAxiosRequestConfig
  ): Promise<T>;
  put<T>(
    url: string,
    data?: any,
    config?: InternalAxiosRequestConfig
  ): Promise<T>;
  patch<T>(
    url: string,
    data?: any,
    config?: InternalAxiosRequestConfig
  ): Promise<T>;
}

그리고 또 response로 받아온 것들 중에서 공통 부분들을 추출해서 인터페이스로 만들었다.

// types/linkbrary.ts 

/* common */

interface APIDataResponse<T> {
  data: T;
}

export interface CommonResponse<T> {
  data: APIDataResponse<T>;
  status: number;
  statusText: string;
}

interceptors에는 요청 성공/실패 여부를 개발 모드에서만 기록할 수 있게 작성했다.

// utils/axios/instance.ts

/* request interceptors */
instance.interceptors.request.use(
  (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
    /**
     * request 직전 공통으로 진행할 작업
     */
    const { method, url } = config;
    logOnDev(`[API] ${method?.toUpperCase()} ${url} | Request`);

    return config;
  },
  (error: AxiosError | Error): Promise<AxiosError> => {
    /**
     * request 에러 시 작업
     */
    return Promise.reject(error);
  }
);

/* response interceptors */
instance.interceptors.response.use(
  (response: AxiosResponse): AxiosResponse => {
    /**
     * http status가 20X이고, http response가 then으로 넘어가기 직전 호출
     */
    const { method, url } = response.config;
    const { status } = response;
    logOnDev(`[API] ${method?.toUpperCase()} ${url} | Response ${status}`);

    return response;
  },
  (error: AxiosError | Error): Promise<AxiosError> => {
    /**
     * http status가 20X가 아니고, http response가 catch로 넘어가기 직전 호출
     */
    if (axios.isAxiosError(error)) {
      const { message } = error;
      const { method, url } = error.config as InternalAxiosRequestConfig;
      const { status, statusText } = error.response as AxiosResponse;

      logOnDev(
        `[API] ${method?.toUpperCase()} ${url} | Error ${status} ${statusText} | ${message}`
      );

      switch (status) {
        case 401: {
          // 로그인 필요 메시지 연결
          break;
        }
        case 403: {
          // 권한 필요 메시지 연결
          break;
        }
        case 404: {
          // 잘못된 요청 메시지 연결
          break;
        }
        case 500: {
          // 서버 문제 발생 메시지 연결
          break;
        }
        default: {
          // 알 수 없는 오류 발생 메시지 연결
          break;
        }
      }
    } else {
      logOnDev(`[API] | Error ${error.message}`);
    }
    return Promise.reject(error);
  }
);

crud에 관한 요청 틀을 미리 만들어두고

// utils/axios/common.ts

/* get 요청 */
export const getRequest = async <T>(
  url: string,
  config?: InternalAxiosRequestConfig
): Promise<T> => {
  const response = await instance.get<CommonResponse<T>>(url, config);
  return response.data.data;
};

/* post 요청 */
export const postRequest = async <T>(
  url: string,
  data: any,
  config?: InternalAxiosRequestConfig
): Promise<T> => {
  const response = await instance.post<CommonResponse<T>>(url, data, config);
  return response.data.data;
};

/* delete 요청 */
export const deleteRequest = async <T>(
  url: string,
  config?: InternalAxiosRequestConfig
): Promise<CommonResponse<T>> => {
  const response = await instance.delete<CommonResponse<T>>(url, config);
  return response;
};

/* put 요청 */
export const putRequest = async <T>(
  url: string,
  data: any,
  config?: InternalAxiosRequestConfig
): Promise<CommonResponse<T>> => {
  const response = await instance.put<CommonResponse<T>>(url, data, config);
  return response;
};

/* patch 요청 */
export const patchRequest = async <T>(
  url: string,
  data: any,
  config?: InternalAxiosRequestConfig
): Promise<CommonResponse<T>> => {
  const response = await instance.put<CommonResponse<T>>(url, data, config);
  return response;
};

구축된 api에 맞게 함수들을 추상화했다.

// utils/axios/linkRequest.ts 

/**
 *
 * @param userId 유저id
 * @returns 유저가 가진 모든 링크 목록
 */
export const getLinks = async (userId: number): Promise<ILink[]> => {
  const response = await getRequest<ILink[]>(`/users/${userId}/links`);
  return response;
};
/**
 *
 * @param userId 유저id
 * @param folderId 폴더id
 * @returns 유저가 가진 특정 폴더의 링크 목록
 */
export const getLink = async (
  userId: number,
  folderId: number
): Promise<ILink[]> => {
  const response = await getRequest<ILink[]>(
    `/users/${userId}/links?folderId=${folderId}`
  );
  return response;
};
/**
 *
 * @param url 저장하려는 주소
 * @param userId 유저id
 * @param folderId 폴더id
 * @returns 생성한 링크 정보
 */
export const createLink = async (
  url: string,
  userId: number,
  folderId?: number
): Promise<ILink[]> => {
  const response = await postRequest<ILink[]>(`/links`, {
    url,
    userId,
    folderId,
  });
  return response;
};
/**
 *
 * @param linkId 삭제하려는 링크id
 * @returns 삭제 성공 여부
 */
export const deleteLink = async (linkId: number): Promise<number> => {
  const response = await deleteRequest<string>(`/links/${linkId}`);
  return response.status; // 삭제 성공시 204 return
};

참고

[TypeScript] Axios TypeScript 적용하여 사용해보자
Axios HTTP Client Using TypeScript
Axios 인터셉터 Typescript로 관리하기

api 요청으로 데이터 수정 시, 실시간 업데이트하기

axios, fetch를 사용한다면 일반적으로는 받아온 데이터를 클라이언트에서 상태로 유지하고, api로 데이터를 수정하면 자연스럽게 상태 변경으로 인해 화면을 리렌더링하도록 한다.

리액트 쿼리를 사용하면 별도의 상태를 둘 필요는 없다.

next js 13 next/navigation의 router.refresh()를 사용하면 단순히 새로고침(reload)이 아니라 서버 요청만 받아오고 영향 없는 클라이언트는 상태를 유지할 수 있도록 할 수 있다. 클라이언트 상태를 유지할 때 refresh를 사용하면 좋다!

// modal.tsx 

const handleClickAddFolder = async () => {
    if (folderNameRef.current.value.length && folderNameRef.current) {
      // api 요청입니다.
      await createFolder(folderNameRef.current.value, userId);
    }

    setTimeout(() => {
      router.refresh();
      setOpenAddFolderModal(false);
    }, 500);
  };

throttle과 debounce

debounce는 계속적으로 발생하는 이벤트 중 마지막 이벤트만 인식하고, throttle은 이벤트가 발생하고 나서 일정 주기마다 이벤트가 발생하도록 한다.

  • 스크롤 이벤트에는 throttle이 필요하다.

나는 throttle이라는 상태를 추가해서, 스크롤을 지연시켰다.

다만 하나 미심쩍은 부분이 있는데, 스크롤 자체는 지연시켰지만, 계속해서 실행되고 있다는 느낌이 강하다.. 해결해야 하는 부분인지 모르겠지만 좀 더 공부해봐야 할 것 같다.

// components/AddLinkField/AddLinkField.tsx 

"use client";

import {
  ForwardedRef,
  forwardRef,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";

import { IFolder, ILink } from "@/types/linkbrary";

import AddLinkBar from "./AddLinkBar";
import styles from "./AddLinkField.module.scss";

interface IAddLinkFieldProps {
  userId: number;
  folders: IFolder[] | [];
  links: ILink[] | [];
  inView: boolean | null;
}

const AddLinkField = forwardRef(function AddLinkField(
  { userId, folders, links, inView }: IAddLinkFieldProps,
  ref: ForwardedRef<HTMLDivElement>
) {
  const FOOTER_HEIGHT = 160;
  const scrollRef = useRef<HTMLDivElement>(null);
  const [throttle, setThrottle] = useState(false);
  const [transition, setTransition] = useState(false);

  const handleScroll = useCallback(() => {
    if (throttle) return;
    if (!throttle) {
      setThrottle(true);
      setTimeout(() => {
        if (scrollRef.current) {
          const currentScrollPosition =
            scrollRef.current.getBoundingClientRect().bottom + window.scrollY; // pageYOffset == scrollY
          const IntersectedFooter = document.body.scrollHeight - FOOTER_HEIGHT;
          setTransition(currentScrollPosition >= IntersectedFooter);
          setThrottle(false);
        }
      }, 300);
    }
  }, [throttle]);

  useEffect(() => {
    handleScroll();
    window.addEventListener("scroll", handleScroll);
    return () => {
      window.removeEventListener("scroll", handleScroll);
    };
  }, [handleScroll]);

  return (
    <div className={styles.observedWrapper}>
      <div
        ref={scrollRef}
        className={`${styles.addLinkContainer} ${
          styles[`${inView ? "view" : "not-view"}`]
        } ${styles[`${transition ? "tr" : "not-tr"}`]}`}
      >
        <AddLinkBar userId={userId} folders={folders} links={links} />
      </div>
      <div className={styles.observed} ref={ref}></div>
    </div>
  );
});

export default AddLinkField;

링크를 추가하는 영역이 footer를 만나면 사라지게 만들었다.

참고

[React] throttle 사용하여 scroll 이벤트 최적화
Debounce 와 Throttle 리액트로 구현하기

에러 해결

middleware.ts 문제 2가지

middleware를 루트 경로에 놓았을 때 다음과 같은 에러가 발생했다.
분명 가능하다고 했는데 찾아보니 버전을 업그레이드하면 일단 해결할 수 있었다.

module parse failed: identifier 'nextresponse' has already been declared

참고

'NextResponse' has already been declared in middleware. #7650

두번째로는, 미들웨어로 접근을 막았을 때 계속해서 아래 에러가 떴다.
로그인이라든지, 접근을 막는 로직 자체는 성공적이었으나 에러를 해결하고 싶었다. 근데 아무리 검색해도 답이 안 나왔다.

역시 정답은 에러 메시지에 있었다.

/api/auth/session으로 접속했을 때, html 페이지가 렌더링된다는 건데 왜그럴까 생각해보니 미들웨어를 잘못 작성했다.

로그인한 유저가 로그인/회원가입 페이지에 접근하지 못하도록 하려고 다음과 같이 작성했는데, 아무래도 /api/auth/sesssion에서 next-auth에서 처리해주는 로직이 있는 것 같다.

// middleware.ts

// 로그인된 유저는 로그인, 회원가입 페이지에 접근 X
  if (pathname.startsWith("/api/auth") && session) {
    return NextResponse.redirect(new URL("/", req.url));
  }

그래서 다음과 같이 변경했다.

// middleware.ts

// 로그인된 유저는 로그인, 회원가입 페이지에 접근 X
  if (pathname.startsWith("/api/auth/login") && session) {
    return NextResponse.redirect(new URL("/", req.url));
  }
  if (pathname.startsWith("/api/auth/register") && session) {
    return NextResponse.redirect(new URL("/", req.url));
  }

에러 메시지를 잘 읽고 생각을 먼저 해보고 검색하는 습관을 가지자...

이미지 에러 처리하기

링크를 추가하면 해당 링크에서 이미지를 추출하는데, 이미지가 없을 수도 있다. 이런 경우 처리를 해줘야 한다. img 태그의 onError 이벤트를 사용하면 된다.

// components/LinkCard/LinkCard.tsx 

const handleImgError = (e: SyntheticEvent<HTMLImageElement, Event>) => {
    e.currentTarget.src = "/assets/image-dummy.png";
  };

<img
  className={styles.image}
  src={link.image_source ?? "/assets/image-dummy.png"}
  onError={handleImgError}
  alt={link.title}
/>

문제는, onError가 클라이언트 단에서 동작한다. 지금 당장은 문제가 해결되지만, 이후 부모 컴포넌트를 ssr로 변경했을 때 문제가 된다.

그래서 img 태그만 따로 컴포넌트로 분리해서 img 태그만 dynamic 처리를 해줬다.

단순히 img 태그만 있지만, 덕분에 이미지 에러 처리 함수 자체도 관심사를 분리할 수 있어서 좋다.

// components/LinkCard/LinkCard.tsx 

const DynamicImage = dynamic(() => import("./DynamicImage"), {
  ssr: false,
  loading: () => <p>Loading...</p>,
});

<div className={styles.cardImgTop}>
          <DynamicImage imgSrc={link.image_source} title={link.title} />
        </div>

// components/LinkCard/DynamicImage.tsx 

"use client";

import { SyntheticEvent } from "react";

import styles from "./LinkCard.module.scss";

const DynamicImage = ({ imgSrc, title }: { imgSrc: string; title: string }) => {
  const handleImgError = (e: SyntheticEvent<HTMLImageElement, Event>) => {
    e.currentTarget.src = "/assets/image-dummy.png";
  };

  return (
    <img
      className={styles.image}
      src={imgSrc ?? "/assets/image-dummy.png"}
      onError={handleImgError}
      alt={title}
    />
  );
};

export default DynamicImage;

참고

img tag onError function doesn't work #14772

new Date not matched 문제

카트 컴포넌트에서 생성날짜를 보여주는데 단순히 new Date()로 하면 서버 컴포넌트에서 출력하는 값과 클라이언트 컴포넌트에서 출력하는 값이 다를 수 있다.

이를 방지하기 위해 toISOString 메소드를 사용하면 된다. 그럼 서버 컴포넌트와 클라이언트 컴포넌트가 렌더링 할 때 날짜를 다르게 렌더링하는 문제를 해결할 수 있다.

참고

How should date strings be handled when rendering on both server and client?
Fix Next.js “Text content does not match server-rendered HTML” React hydration error

Error: An error occurred in the Server Components render. he specific message is omitted in production builds to avoid leaking sensitive details.

로컬에서는 정상적이어서 몰랐는데, 아무래도 서버 컴포넌트에서 데이터 가져오는 코드는 훅으로 빼면 안 되는 듯 하다.

훅으로 빼두었던 getCurrentUser 코드를 다시 page에 주입했더니 빌드가 성공했다..

// utils/getCurrentUser.ts 

import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { getUser } from "@/lib/axios/userRequest";
import { tempUserDatas } from "@/utils/constants";
import { getServerSession } from "next-auth";

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

export default async function getCurrentUser() {
  try {
    const session = await getServerSession(authOptions);

    if (!session?.user?.email) {
      return null;
    }

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

    if (!currentUser) {
      return null;
    }

    const userId = tempUserDatas[currentUser.id];

    const user = await getUser(userId);

    return user;
  } catch (error) {
    return null;
  }
}

참고

Error: An error occurred in the Server Components render. he specific message is omitted in production builds to avoid leaking sensitive details. #44463

prisma 배포시

vercel에 빌드할 때 다음과 같은 에러가 뜨면

PrismaClientInitializationError: Prisma has detected that this project was built on Vercel, which caches dependencies. This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered. To fix this, make sure to run the prisma generate command during the build process.

build 스크립트에 prisma generate를 추가해주면 된다.

"build": "prisma generate && next build"

참고

[ERROR] Vercel 배포시 Prisma 에러 - 해결

(일반) 배포가 안 될 때

로컬로에서 npm run build도 해보고, npm install도 해봐야 한다..

아직 해결하지 못한 문제 => 해결완료!!!

배포 시 계속 문제가 생겨서 어떻게 해결해야 할지 모르다가


vercel에서 프로젝트의 logs > function에서 에러 상세 내용을 확인할 수 있었다.

[...nextauth]/route.ts에 secret을 추가해주고 배포 환경변수도 추가해줬더니 해결했다..

참고

Please define a secret in production. #3245

또 에러

prisma에 넣은 DATABASE_URL을 인식하지 못하는 것 같다.. 환경 변수로도 넣었는데 왜..?

친절하게도 쿼리스트링으로 에러 메시지를 보내준다. 디코딩하니 이렇게 뜨는데, 환경변수를 설정했음에도 뭐가 문제일까..

그냥 이름이 겹쳐서였다. 변경해주고 재시작하니 된다!

참고

Error: Environment variable not found: DATABASE_URL. #54

profile
https://medium.com/@wooleejaan

0개의 댓글