본문 바로가기

Nestjs 설정하기

@Jeeqong 2024. 9. 5. 10:37
반응형

1. next’s 설치

npx create-next-app@latest 폴더명

설정 질의는 버전별로 좀 다른듯

cd 폴더명
❯ npx create-next-app@latest --ts
✔ What is your project named? … nextjs-init
✔ Would you like to use ESLint with this project? … No / Yes
✔ Would you like to use `src/` directory with this project? … No / Yes
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
✔ What import alias would you like configured? … @/*

 

2 . TypeScript 설정

이건 설치할떄기본으로 해줌

tsconfig.json 생성 , typescript @types/react @types/node 설치하기

touch tsconfig.json 
npm install --save-dev typescript @types/react @types/node

 

3. Tailwind CSS 또는 Styled Components 설정

npm install -D tailwindcss postcss autoprefixer 
npx tailwindcss init -p
npm install -g sass // sass 설치
npm install -D @tailwindcss/forms // form 설치
npm install --save-dev sass

 

3.1 global.css 수정

globals.css는 _app.tsx에서 import할 경우 모든 page를 대상으로 적용하는 css이다.

Tailwind를 사용하는 경우에는 이 내용을 전부 지우고 다음과 같이 수정한다.

@tailwind base; @tailwind components; @tailwind utilities;

 

3.2  form 설치 후 추가

스타일 공통정의에 유용함

: A plugin that provides a basic reset for form styles that makes form elements easy to override with utilities.

: 유틸리티를 사용하여 양식 요소를 쉽게 재정의할 수 있도록 양식 스타일을 기본적으로 재설정하는 플러그인입니다.

https://github.com/tailwindlabs/tailwindcss-forms

 

GitHub - tailwindlabs/tailwindcss-forms: A plugin that provides a basic reset for form styles that makes form elements easy to o

A plugin that provides a basic reset for form styles that makes form elements easy to override with utilities. - tailwindlabs/tailwindcss-forms

github.com

https://tailwindcss-forms.vercel.app/

 

@tailwindcss/forms examples

 

tailwindcss-forms.vercel.app

 

// tailwind.config.js
module.exports = {
  theme: {
    // ...
  },
  plugins: [
    require('@tailwindcss/forms'),
    // ...
  ],
}
tailwind.config.ts

 

ts 파일이 js 를 포함할때 에러날수있음 js 파일 삭제필요

또는 js 만 쓰는경우도 있음

typescript 를 쓰므로 js 삭제함

 

4.NextAuth.js 설정 (로그인 구현)

npm i bcryptjs next-auth
npm i --save-dev @types/bcryptjs

참고 : https://medium.com/@pether.maciejewski/nextauth-credentials-easy-signup-login-with-email-password-next-js-7e8f043b2084

 

openssl rand -base64 32

 

4.1 NextAuth 설정 파일 생성

// src/pages/api/auth/[...nextauth].ts

import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';

export default NextAuth({
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: "Email", type: "text" },
        password: { label: "Password", type: "password" }
      },
      async authorize(credentials) {
        const res = await fetch(`${process.env.NEXTAUTH_URL}/api/auth/login`, {
          method: 'POST',
          body: JSON.stringify({
            email: credentials?.email,
            password: credentials?.password,
          }),
          headers: { 'Content-Type': 'application/json' },
        });

        const user = await res.json();

        // 로그인 성공 시 사용자 정보 반환
        if (res.ok && user) {
          return user;
        }

        // 로그인 실패 시 null 반환
        return null;
      },
    }),
  ],
  pages: {
    signIn: '/auth/signin', // 로그인 페이지 경로
  },
  session: {
    strategy: 'jwt', // JWT를 사용한 세션 관리
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.email = user.email;
      }
      return token;
    },
    async session({ session, token }) {
      session.user.id = token.id;
      session.user.email = token.email;
      return session;
    },
  }
});

 

4.2 로그인 페이지 생성

pages/auth/signin.tsx 파일을 생성하여 로그인 페이지를 만듭니다.

// src/app/auth/signin/page.tsx
'use client';

import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default function SignIn() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const router = useRouter();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const res = await signIn('credentials', {
      redirect: false,
      email,
      password,
    });

    if (res?.error) {
      setError(res.error);
    } else {
      router.push('/dashboard'); // 로그인 후 대시보드로 이동
    }
  };

  return (
    <div>
      <h1>Sign In</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
        />
        {error && <p>{error}</p>}
        <button type="submit">Sign In</button>
      </form>
    </div>
  );
}

 

4.3 SessionProvider로 세션 전역 관리

// src/app/layout.tsx
'use client';

import { SessionProvider } from 'next-auth/react';
import './globals.css';
import { ReactNode } from 'react';

export default function RootLayout({
  children,
  session, // session prop을 받습니다.
}: {
  children: ReactNode;
  session: any;
}) {
  return (
    <html lang="en">
      <body>
        <SessionProvider session={session}>
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}

 

4.4 사용자인증 로직 추가 필요

- 서버  or db 연결 및 로직추가

npm install uuid

 

5. Vercel 배포 및 PostgreSQL을 Vercel에서 호스팅하고 사용

5.1 @vercel/postgres 설치

npm install @vercel/postgres

 

5.2. PostgreSQL 연결 및 데이터베이스 설정

https://nextjs.org/learn/dashboard-app/setting-up-your-database#create-a-postgres-database

 

App Router: Setting Up Your Database | Next.js

Setup a database for your application and seed it with initial data.

nextjs.org

 

.env 파일에 데이터베이스 연결 정보 추가

DATABASE_URL=postgres://<username>:<password>@<hostname>:<port>/<database_name>

 

.env.local 파일 설정

NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:3000/api
  • .env.local (로컬 개발 환경)
  • .env.development (개발 환경)
  • .env.production (프로덕션 환경)
  • .env (기본 환경)

 

6. Middleware 와 approute 로 인증세션 및 페이지 이동 흐름

  • App Router와 Middleware는 서로 상호보완적인 방식으로 사용가능
  • App Router는 페이지와 컴포넌트의 구조와 데이터 페칭을 처리하는 방식
  • Middleware는 경로 기반의 전역적인 요청 처리나 인증을 미리 확인하는 용도로 사용
src/middleware.ts //프로젝트 루트에 middleware.ts 파일을 생성

 

6.1. NextAuth.js의 getToken 사용

 

6.2. Middleware 코드 예시

// src/middleware.ts

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

// 보호된 경로에 대해 인증을 처리
export async function middleware(req: NextRequest) {
  // 토큰 가져오기 (세션 확인)
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });

  // 요청 URL
  const { pathname } = req.nextUrl;

  // 인증되지 않은 사용자가 보호된 페이지에 접근하려고 할 때 로그인 페이지로 리디렉션
  if (!token && pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/auth/signin', req.url));
  }

  // 세션이 있는 경우 요청을 계속 진행
  return NextResponse.next();
}

// 보호할 경로 설정
export const config = {
  matcher: ['/dashboard/:path*', '/profile/:path*'], // 보호할 경로 설정 (예: 대시보드, 프로필)
};

 

6.3. 환경 변수 설정

//.env.local 파일에 NEXTAUTH_SECRET 추가
NEXTAUTH_SECRET=your-secret-key

 

7. NextAuth.js 설정을 하나의 모듈로 분리해서 재사용 가능하게 하기

7.1. authOptions 분리하기

// src/lib/auth.ts

import CredentialsProvider from 'next-auth/providers/credentials';
import NextAuth from 'next-auth';

export const authOptions = {
  providers: [
    CredentialsProvider({
      name: 'Credentials',
      credentials: {
        email: { label: "Email", type: "email" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials) {
        const res = await fetch(`${process.env.NEXTAUTH_URL}/api/auth/login`, {
          method: 'POST',
          body: JSON.stringify({
            email: credentials?.email,
            password: credentials?.password,
          }),
          headers: { 'Content-Type': 'application/json' },
        });

        const user = await res.json();

        if (res.ok && user) {
          return user;
        }

        return null;
      },
    }),
  ],
  session: {
    strategy: 'jwt',
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.email = user.email;
      }
      return token;
    },
    async session({ session, token }) {
      session.user.id = token.id;
      session.user.email = token.email;
      return session;
    },
  },
};

export default NextAuth(authOptions);

 

7.2 NextAuth API 라우트에서 authOptions 가져오기

// src/pages/api/auth/[...nextauth].ts
import NextAuth from 'next-auth';
import { authOptions } from '@/lib/auth'; // authOptions를 가져옴

export default NextAuth(authOptions);

 

7.3. 서버 컴포넌트에서 authOptions 사용하기

// src/app/dashboard/page.tsx
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth'; // authOptions를 가져옴
import { redirect } from 'next/navigation';

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

  if (!session) {
    redirect('/auth/signin'); // 세션이 없으면 로그인 페이지로 리디렉션
  }

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Welcome, {session.user?.email}</p>
    </div>
  );
}

 

7.4. 클라이언트 컴포넌트와 서버 컴포넌트 분리

// src/app/dashboard/DashboardClient.tsx (클라이언트 컴포넌트)
'use client';

import { signOut } from 'next-auth/react';

export default function DashboardClient({ session }) {
  return (
    <div>
      <p>Welcome, {session.user?.email}</p>
      <button onClick={() => signOut({ callbackUrl: '/auth/signin' })}>
        Sign Out
      </button>
    </div>
  );
}

 

7.5 클라이언트 컴포넌트에서 이벤트 처리

// src/app/dashboard/page.tsx (서버 컴포넌트)

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth'; // NextAuth 설정
import { redirect } from 'next/navigation';
import DashboardClient from './DashboardClient'; // 클라이언트 컴포넌트 가져옴

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

  if (!session) {
    redirect('/auth/signin'); // 세션이 없으면 로그인 페이지로 리디렉션
  }

  // 서버 컴포넌트에서 세션을 전달하고, 클라이언트 컴포넌트로 렌더링
  return (
    <div>
      <h1>Dashboard</h1>
      <DashboardClient session={session} />
    </div>
  );
}

 

 

8. Middleware에서 세션을 확인하고, 세션이 없으면 로그인 페이지로 리디렉션하면서 **callbackUrl**을 설정하여 로그인 후 원래 페이지

8.1. Middleware에서 동적으로 callbackUrl 설정

// src/middleware.ts
import { NextResponse } from 'next/server';
import { getToken } from 'next-auth/jwt';

export async function middleware(req) {
  const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
  const { pathname } = req.nextUrl;

  // 세션이 없고 보호된 경로에 접근하려고 할 때
  if (!token && pathname !== '/auth/signin') {
    const url = new URL('/auth/signin', req.url); // 로그인 페이지로 리디렉션
    url.searchParams.set('callbackUrl', req.url); // 원래 요청한 경로를 callbackUrl로 설정
    return NextResponse.redirect(url); // 로그인 후 돌아올 경로 설정
  }

  return NextResponse.next(); // 세션이 있으면 요청을 계속 진행
}

export const config = {
  matcher: ['/pages/:path*'], // 모든 보호된 경로 설정
};

 

8.2. 로그인 페이지에서 callbackUrl 처리

// src/pages/auth/signin.tsx
'use client';

import { signIn } from 'next-auth/react';
import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

export default function SignIn() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const router = useRouter();
  const searchParams = useSearchParams();
  
  const callbackUrl = searchParams.get('callbackUrl') || '/dashboard'; // 로그인 후 리디렉션할 URL

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const res = await signIn('credentials', {
      redirect: false,
      email,
      password,
    });

    if (res?.error) {
      setError(res.error);
    } else {
      // 로그인 성공 시 callbackUrl로 이동
      router.push(callbackUrl);
    }
  };

  return (
    <div>
      <h1>Sign In</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
        />
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          placeholder="Password"
        />
        {error && <p>{error}</p>}
        <button type="submit">Sign In</button>
      </form>
    </div>
  );
}

 

8.3. 서버 컴포넌트에서 동적 리디렉션

// src/pages/usesetting.tsx
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth'; // NextAuth 설정
import { redirect } from 'next/navigation';

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

  if (!session) {
    // 로그인 페이지로 리디렉션하면서 원래 경로를 callbackUrl로 설정
    redirect(`/auth/signin?callbackUrl=/pages/usesetting`);
  }

  return (
    <div>
      <h1>User Setting</h1>
      <p>Welcome, {session.user?.email}</p>
    </div>
  );
}
반응형
Jeeqong
@Jeeqong :: JQVAULT

Jeeqong's vault : 정보/기록을 쌓아두는 공간 웹개발 포스팅 일상 리뷰를 기록하는 공간입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차