본문 바로가기

프로젝트 캠프 : Next.js 2기

[유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 - 프로젝트주차 1주차 후기

 

일단 프로젝트 시작에 앞서 수업 때 했던 내용인데 정리하지 못했던 부분들을 모아보려고 한다.

1. MongoDB 설정

Database를 누르면 클러스터의 정보가 보이는데, 커넥트 버튼을 누르고 Driver를 클릭해 보이는 주소를 복사한다. 이미 연결한 이력이 있다면 선택하는 부분은 나오지 않고 바로 주소가 나올것이다.

이런 형태의 주소인데 정보를 가린 첫번째 부분은 userName이 들어가고 <password> 부분에 <>을 포함해서 지워주고 클러스터를 생성할 때 설정한 비밀번호를 입력해야한다.

 

오른쪽 버튼을 클릭해서 복사한 뒤 env에 MONGODB_URL= 다음 입력해준다. 강의에선 첫줄의 느낌표 앞에 test라는 키워드를 붙여줬는데, test라는 공간에다가 DB를 생성하겠다라는 의미다.

 

아래 명령어로 패키지를 설치해주고,

npm i mongoose

 

아래 파일에 코드를 넣어준다.

// libs/db.ts

import mongoose from "mongoose"

export const connectToDB = async () => {
  try {
    if (mongoose.connection.readyState !==0) {
    console.log("Already Connected to DB");
    return;
    }
    await mongoose.connect(process.env.MONGODB_URL || "");
    console.log("Connected to DB";
    } catch (error) {
      throw new Error(String(error));
    }
  }

 

서버를 구동하면 DB에 연결된다. 연결이 수립되면 readyState 값이 1이 되어 재연결을 시도하지 않도록 예외처리를 해준 코드이다.

 

테이블에 대한 명세서를 정해 놓는 스키마를 생성해준다.

// libs/schemas/posts.ts

import { TPost } from "@/types/post";
import mongoose from "mongoose";
const postSchema = new mongoose.Schema<TPost>({
  name: {
    type: String,
  },
  profile: {
    type: String,
  },
  title: {
    type: String,
    required: true,
  },
  description: {
    type: String,
    required: true,
  },
  thumbnail: {
    type: String,
    required: true,
  },
  category: {
    type: String,
    required: true,
  },
  datetime: {
    type: Date,
    required: true,
  },
});

export const Post = mongoose.models?.Post || mongoose.model("Post", postSchema);

 

더보기

mongoose 지정 가능 타입

 

타입 지정 (Type)

String, Number, Date, Buffer, Boolean, Mixed, ObjectId, Array, Decimal128, Map, Schema.Types
예: { name: String }
필수 항목 (required)

필드가 필수인지 여부를 지정합니다.
예: { name: { type: String, required: true } }
기본값 (default)

필드의 기본값을 지정합니다.
예: { age: { type: Number, default: 0 } }
유일성 (unique)

필드 값이 유일해야 함을 지정합니다.
예: { email: { type: String, unique: true } }
인덱스 (index)

필드에 인덱스를 생성합니다.
예: { email: { type: String, index: true } }
유효성 검사 (validate)

필드 값에 대한 유효성 검사를 지정합니다.
예: { age: { type: Number, validate: value => value >= 0 } }
길이 제한 (maxlength, minlength)

문자열 필드의 최대/최소 길이를 지정합니다.
예: { name: { type: String, maxlength: 50 } }
열거형 (enum)

문자열 필드가 가질 수 있는 값들을 제한합니다.
예: { role: { type: String, enum: ['user', 'admin'] } }
일치 (match)

문자열 필드가 정규표현식에 맞아야 함을 지정합니다.
예: { email: { type: String, match: /.+\@.+\..+/ } }
맞춤 메시지 (custom error messages)

유효성 검사 실패 시 사용자 정의 오류 메시지를 설정합니다.
예: { age: { type: Number, min: [18, 'Too young'] } }

 

위의 스키마에 정의된 TPost 예제이다.

 

// types/post.d.ts

export type TPost = {
  name: string;
  profile: string;
  id: string;
  title: string;
  category: string;
  description: string;
  thumbnail: string;
  detetime: Date;
};

 

 

2. Github 로그인

next.js 공식 홈페이지에서 Learn Next.js -> start Learning 클릭해서 페이지 이동하고, 왼쪽 햄버거바에서 15번 챕터로 오게되면, 참고할 수 있는 문서가 있다.

https://nextjs.org/learn/dashboard-app/adding-authentication

 

Learn Next.js: Adding Authentication | Next.js

Add authentication to protect your dashboard routes using NextAuth.js, Server Actions, and Middleware.

nextjs.org

 

npm i next-auth@beta

 

아래명령어는 컴퓨터 환경에 따라 동작하지 않을 수 있는데, 동작한다면 터미널의 문자열을 env에 넣어주고 동작하지 않는다면 1234와 같은 임의의 문자열을 넣어도 상관없다.

openssl rand -base64 32

 

// .env
AUTH_SECRET=your-secret-key

 

// src/auth.config.ts

import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;

 

 

authorized가 인증을 처리하는 중요한 부분인데, next.auth를 사용할 때 어떻게 사용할 지 나타내는 콜백함수이다. 예제에선 Dashboard를 기본으로 사용하게 되어있는데, /dashboard 라는 경로를 보호하겠다라는 의미이다. api폴더 아래에 dashboard/page.tsx를 생성하여 코드를 짜면 그 페이지로 이동하게 된다.

 

// src/middleware.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export default NextAuth(authConfig).auth;
 
export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};

 

// src/auth.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [Credentials({})],
});

 

위의 예제에는 Credentials가 있는데, 저 부분은 일반인증이다. id와 password를 받아서 서버에 등록하는 회원가입 과정을 거치는 방식인데, 우리는 필요가 없기때문에 제거해도 된다.

 

다음으로는 authjs.dev 문서를 참고해야한다.

get start -> Authentication ->OAuth 를 클릭해서 필요한 OAuth를 선택하여 참고하면 된다. 우리는 github를 연결할 것이다.

 

https://authjs.dev/getting-started/authentication/oauth

 

OAuth Providers

Authentication for the Web

authjs.dev

 

// .env

AUTH_GITHUB_ID={CLIENT_ID}
AUTH_GITHUB_SECRET={CLIENT_SECRET}

 

이 정보를 얻기 위해서 github에 접속해 오른쪽 위 프로필 -> settings -> 왼쪽 메뉴 하단의 <> Developer settings -> OAuth Apps -> new OAuth app

 

위의 authjs 공식문서를 내리다보면 중간에 사용하는 언어에 따른 탭이 있는데, Next.js가 가장 앞에 기본값으로 설정되어 있기에 그대로 참고하면 된다.

 

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

import { handlers } from "@/auth"
export const { GET, POST } = handlers

 

그런다음 auth.ts의 코드를 수정해 handlers를 export 해주면 route.ts의 에러가 사라진다.

// src/auth.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export const { auth, signIn, signOut, handlers } = NextAuth({
  ...authConfig,
});

 

 

공식문서에서 확인할 수 있는 콜백 url인데 /api부터 복사해서 github 설정에 넣어준다.

 

 

name과 description은 아무렇게난 설정해도 무방하고 url을 유의해서 설정하면 된다.

그 다음 생성되는 마크를 누르고 들어가서 Client ID를 확인해서 env에 넣어주고 Client secret를 생성할 수 있는데, 생성한 시점에만 확인할 수 있으니 보관을 잘 해야한다.

 

update application으로 페이지에서 빠져나가면 완료이다.

다름으로 auth.config을 수정해준다. 깃허브를 임포트하고 providers로 GitHub를 제공하도록 수정한다.

// src/auth.config.ts

import type { NextAuthConfig } from 'next-auth';
import GitHub from "next-auth/providers/github"
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
  providers: [
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID
      clientSecret: process.env.AUTH_GITHUB_SECRET
      })
     ],
} satisfies NextAuthConfig;

 

 

그런 다음 server라는 폴더를 생성해 서버액션을 이용할 준비를 한다.

// src/server/users.action.ts

"use server"

import { signIn } from "@/auth"

export async function githubLogin() {
  await signIn("github");
}

 

client 페이지에서 서버액션을 사용할 수 있게 해주는 훅인 useTransition을 이용하여 로그인 페이지에서 Oauth를 사용할 수 있다. 아래와 같이 퍼블리싱된 컴포넌트에 코드를 추가하게 되면 signIn이라는 메소드는 우리가  auth폴더에 설정한대로  next-auth 패키지에서 제공하기 때문에 매개변수로 github를 설정한 것만으로도 깃허브 providers를 통해 페이지에서 사용할 수 있게 되는 것이다. 첫번째 인자인 pending은 안 쓸경우 _로 처리하는 게 일반적이다.

// components/auth/LoginForm.tsx

const [pending, startTransition] = useTransition();

<button onClick={() => startTransition(async() => await githubLogin())} >

 

 

이로써 로그인이 가능하게 되었는데, auth.config.ts를 수정하여 dashboard가 아닌 다른 페이지로 이동하도록 설정할 수 있다. 원래 코드에서 else if문을 지워줘서 보호되고 있는 dashboard라는 페이지가 아닌 홈으로 이동하게 될 것이다.

// src/auth.config.ts

import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } 
      return true;
    },
  },
  providers: [], // Add providers with an empty array for now
} satisfies NextAuthConfig;

 

 

 

로그인 시 이동할 페이지 설정을 위해 헬퍼함수를 만들건데, 

// libs/onlyNotLogin.ts

import { auth } from "@/auth";
import { redirect } from "next/navigation";

// 로그인 하지 않은 사람만
export const onlyNotLogin = async () => {
  const session = await auth(); // 로그인하면 session에 값 있고, 하지않으면 값 없음
  if (session) redirect("/");
}
// libs/onlyLogin.ts

import { auth } from "@/auth";
import { redirect } from "next/navigation";

// 로그인 한 사람만
export const onlyLogin = async () => {
  const session = await auth(); // 로그인하면 session에 값 있고, 하지않으면 값 없음
  if (!session) redirect("/");
}

 

// app/(auth)/layout.tsx
import Header from "@/components/common/Header";
import { onlyNotLogin } from "@/libs/onlyNotLogin";

export default async function layout({
  children,
} : {
  children: React.ReactNode;
})  {
  await onlyNotLogin();
  return (
    <>
      <Header>{Children}</Header>
    </>
  )

 

이런 방식으로 각 페이지의 layout.tsx에 처리해주면 사용자가 접근할 수 있는 페이지와 접근 불가능한 페이지를 설정할 수 있다.

 

 

libs 아래에 getSession.ts를 만들어주면 쉽게 불러와서 사용할 수 있는데,

import { auth } from "@/auth"

export const getSesion = async () => {
  const session = await auth();
  return session;
}

 

 

이런식으로 만들어서 컴포넌트에서 세션유무로 더 세밀하게 노출할 컨텐츠를 선택할 수 있다. 아래 코드에 Logout버튼도 추가했는데, 로그아웃 역시 서버액션으로 구현할 수 있다.

// components/common/Header.tsx

import { getSession } from "@/libs/getSession";

const session = await getSession()

 return (
   {session && (
     ...
   )}
   {!session && (
     ...
   )}
   
   {session && (
     <li>
     <form action={logout}>
       <button>Logout</button>
     </form>
     </li>
   )}       
 )

 

이런식으로 서버액션에 코드를 추가해준 모습이다.

// src/server/users.action.ts

"use server"

import { signIn } from "@/auth"

export async function githubLogin() {
  await signIn("github");
}

export async function githubLogin() {
  await signIn("github");
}

 

Server Action
  • Server Actions은 React Actions 위에 구축된 Next.js의 알파 기능으로, 사용자 상호 작용에 대한 응답으로 async 코드를 실행할 수 있다. 프론트엔드 API 용 백엔드를 구축하는 훌륭한 방법입니다.
  • 더 이상 API Routes 를 사용하지 않아도 되며, Server Actions를 사용하면 서버에 추가할 데이터를 fetch하거나 서버에서 mutations(생성, 업데이트, 삭제)하는 것이 훨씬 쉬워집니다.
  • 서버 컴포넌트 내에서 서버 측 비즈니스 로직을 클라이언트에서 직접 호출할 수 있도록 하는 기능으로, 주로 POST 요청으로 쓰인다.

- 특징

React에도 존재하는 기능이지만, 서버컴포넌트 같은 React에서는 제대로 작동하진 않고 Nextjs같은 프레임워크의 도움이 필요하다. form의 action속성에 값으로 할당할 수 있는데, 보통 action속성에는 요청을 보낼 경로가 설정되는 것이 보통이기에 다소 생소할 수 있다. 요청이 전송되면 Nextjs에서 요청을 생성해서 웹사이트를 제공하는 Nextjs서버로 보내게 되고 form의 제출을 제어할 수 있게 된다. 클라이언트측이 아닌 서버에서 쓸 수 있고, 그것은 Nextjs의 SSR을 유지하는 데에 도움이 된다.