로딩,에러,404페이지 만들기

|

Next.js에서 <Link>등으로 페이지 이동시 새로고침이아니라 꼭 필요한 부분만 바뀌도록 세팅되어있다.

loading.js

리액트에서 <Suspense fallback={<h4>loading../</h4>}>이거랑 똑같은 역할을 해준다.
Next.js에선 loading.js 파일에 적으면 자동으로 Suspense 기능을해준다.

page.js 옆에 loading.js라는 이름으로 파일을 만들면 page.js 로드전에 loading.js안의 내용을 미리 보여준다.

Ex) loading.js, 컴포넌트 만들고 그 안에 로딩중에 보여줄 UI를 넣어주면된다.
참고로 client component로도 사용가능하다.

// loading.js
export default function Loading() {
  return <h4>Loading...</h4>;
}

경로는 /app/loading.js 이렇게 파일을 넣어줘도된다.
page.js옆에 loading.js 파일이없으면 상위폴더로 이동하면서 loading.js를 찾는다. 그러면서 가장 가까운 loading.js파일을 찾아서 적용해준다.

error.js

loading.js와 마찬가지로 page.js옆에 error.js파일을 만들어주면된다.

Ex)

error.js;

('use client');

export default function Error({ error, reset }) {
  return (
    <div>
      <h4>Error 페이지 예시</h4>
      <button
        onClick={() => {
          reset();
        }}
      >
        다시시도
      </button>
    </div>
  );
}

몇가지 참고사항이 있다.

  • client component로만 작성가능하다.
  • error라는 props출력해보면 에러내용을 알려준다.
  • reset라는 props를 () 붙여서 실행하면 해당 페이지를 다시 로드해준다.

경로는 /app/error.js 이렇게 파일을 넣어줘도된다.
page.js옆에 error.js 파일이없으면 상위폴더로 이동하면서 error.js를 찾는다. 그러면서 가장 가까운 error.js파일을 찾아서 적용해준다.

not-found.js

없는 URL로 접속하는 유저에게 보여주는 페이지다.
Next.js에선 자동으로 404 page not found페이지를 보여주기때문에 직접 만들 필요는 없다

Ex)

not - found.js;

export default function NotFound() {
  return <h4>404 에러페이지</h4>;
}
  • not-found.js 파일을 page.js 옆에 만들어두고 404페이지를 꾸며준다.
import { notFound } from 'next/navigation';

export default async function Detail(props) {
  const db = (await connectDB).db('forum');
  let result = await db
    .collection('post')
    .findOne({ _id: new ObjectId(props.params.id) });

  if (result == null) {
    return notFound();
  } else {
    return 상세페이지;
  }
}
  • 특정 상황에서 404페이지를 보여주고 싶은 경우 notFound()라는 함수를 실행시켜준다.
  • page.js대신 not-found.js를 보여주게된다.
  • 계속 상위폴더로 찾아가면서 가장 가까운 not-found.js를 보여주려고 하기때문에 app폴더에 not-found.js 하나만 만들어놔도 쉽게 404페이지를 보여줄 수 있다.

next.js사용중에 css,html요소 렌더링이안됬다.

|

1.

next.js를 사용중에 css적용과,html요소 렌더링이 안됬다.
요소를 추가하고 css를 새롭게 적용시킬때마다 서버를 껏다 키고, 사이트를 새로고침해줘야 제대로 적용이되었다. Ex)

// 기존html요소
<div>요소1<div>

// 내가 하드코딩해서 추가한 요소
<div className='red'>요소1 <span>제목</span><div>

2

해결은 간단했다.
위의 적용이안되는 페이지는 클라이언트 컴포넌트였는데,
여태 서버컴포넌트를 사용하다보니 페이지의 대소문자를 구분 안해줬었는데 클라이언트컴포넌트는 대문자로 작성해줘야 서버가 실시간으로 알아듣고 반응을 해주는거였다.

// 내보내고
export default Comment;

// 불러오고
import Comment from './Comment';

fetch,'Content-Type':'application/json'

|

코드를 치면서 알게된거를 정리한거라 정확성은 좀 떨어질 수 있습니다. 지나치다 잘못된 점을 지적해주시면 오히려 감사합니다.

개요

fetch를 사용해서 서버에게 데이터를 보내던 중에 깨닫게된점을 정리하려고한다.

코드 먼저 살펴보자. 이 코드는 ajax인 fetch를 사용하면 많이 익숙한 코드이다.

fetch('/api/comment/new', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    comment,
    postId,
  }),
});

여기서 headers:{} 부분과 JSON.stringify() 부분을 살펴보면..

JSON.stringify

object나 array 데이터를 보낼때는 JSON.stringify()를 사용해줘야한다.

서버에게 일반적인 데이터 문자나 숫자를 보낼때는 JSON.stringify()를 사용할 필요없다. 바로 그냥 보내주면된다.

Ex) 문자열을 서버에 보내줄때는 사용할 필요가 없다.

// 1
fetch('/url', {
  method: 'POST',
  body: '띠용',
});

Ex) object나 array데이터를 보낼때는 사용해줘야한다..

// 2
fetch('/url', {
  method: 'POST',
  body: JSON.stringify({
    comment: '안녕하세요',
    postId: '1',
  }),
});

그럼 이제 서버에서 유저가 보낸 데이터를 읽을때 어떤 모습일까?

1번 코드

// 서버에서 실행한 코드 출력
console.log(req.body); // '띠용'

2번 코드

// 서버에서 실행한 코드 출력
console.log(req.body); // {'comment':'안녕하세요','postId':'1'}

이런 모습이다.
2번 코드는 프론트에서 JSON.stringify()로 감싸주었기때문에 모든 객체가 문자열화되어있다. 이거를 데이터로 사용하려면 JSON.parse()를 해줘야하는 번거로움(?)이 있는데 이게 귀찮으면 다음을 사용해주자

‘Content-Type’:’application/json’

프론트에서 JSON.stringify()로 감싸줘야하는 데이터를 보내줬다면, headers에 ‘Content-Type’:’application/json’를 써서 데이터를 보내주자.

Ex)

fetch('/api/comment/new', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    comment,
    postId,
  }),
});

이렇게 보내주면 서버에서 따로 JSON.parse()로 문자열화된 객체를 다시 되돌리는 짓을 안해도된다.

회원기능 만들기 아이디/비번 + JWT 사용

|

아이디/비번 방식으로 로그인하려면, Next-auth 라이브러리 설정에서 Credentials provider를 선택하면 된다.
다만 이 경우 session 방식 말고 강제로 JWT방식만 사용하도록 셋팅 되어있기때문에, 기존에 세션방식으로 짠 코드가있다면 다 무용지물이된다.
라이브러리 제작자 말로는 ‘개발자가 직접 아이디,비밀번호를 관리하면 위험하니 사용못하게 막아놨다’고 한다.

과정은 두가지가 끝이다.

  1. 회원가입 페이지에서 아이디/비번을 서버로 제출
  2. 서버는 그걸 DB에 저장

끝.

회원가입페이지에서 유저가 아이디와 비밀번호 제출

// /app/register/page.js;

export default function Register() {
  return (
    <div>
      <form method='POST' action='/api/auth/signup'>
        <input name='name' type='text' placeholder='이름' />
        <input name='email' type='text' placeholder='이메일' />
        <input name='password' type='password' placeholder='비번' />
        <button type='submit'>id/pw 가입요청</button>
      </form>
    </div>
  );
}

회원가입페이지다.
이름,이메일,패스워드를 입력후 서버에 전송하는 페이지다.
요즘은 아이디말고 이메일 쓰는게 유행이라고하는데, OAuth로 로그인시에도 유저 아이디는 없고 이메일만 나와서 이메일 쓰는건 일종의 일관성 유지 용도도 있다고한다.

참고로 이메일말고, 아이디만 써도 상관은없지만, OAuth는 중복방지위해서 이메일이 필요하기때문에 email란도 있어야 아마 나중에 문제가 없을거라고 한다. (소셜로그인 기능 구현안할거면 상관없다.)

서버는 그걸 DB에 저장

npm install bcrypt

암호화 라이브러리인 bcrypt를 이용해서 DB에 비밀번호를 암호화시켜서 저장해주자.

// /pages/api/auth/signup.js;

import { connectDB } from '@/util/database';
import bcrypt from 'bcrypt';

export default async function handler(요청, 응답) {
  if (요청.method === 'POST') {
    const hash = await bcrypt.hash(요청.body.password, 10);
    요청.body.password = hash;

    let db = (await connectDB).db('forum');
    await db.collection('user_cred').insertOne(요청.body);
    응답.status(200).json('성공');
  }
}

폼으로 전송된 유저정보를 DB에 저장하는 서버 기능 코드이다.
bcrypt를 이용해서 비밀번호를 암호화 시켜줬고,
유저 정보를 object로 만들어서 user_cred 컬렉션에 넣어줬다.

Credentials provider 설정하기

Next-auth 설정에 Credentials provider도 설정해놓으면 아이디/비밀번호 로그인이 가능하다.

복붙.

import { connectDB } from '@/util/database';
import { MongoDBAdapter } from '@next-auth/mongodb-adapter';
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';

// 여기서부터 새롭게 추가했음
import CredentialsProvider from 'next-auth/providers/credentials';
import bcrypt from 'bcrypt';

export const authOptions = {
  providers: [
    GithubProvider({
      clientId: 'Github에서 발급받은 ID',
      clientSecret: 'Github에서 발급받은 Secret',
    }),

    // 이부분부터  추가
    CredentialsProvider({
      //1. 로그인페이지 폼 자동생성해주는 코드
      name: 'credentials',
      credentials: {
        email: { label: 'email', type: 'text' },
        password: { label: 'password', type: 'password' },
      },

      //2. 로그인요청시 실행되는코드
      //직접 DB에서 아이디,비번 비교하고
      //아이디,비번 맞으면 return 결과, 틀리면 return null 해야함
      async authorize(credentials) {
        let db = (await connectDB).db('forum');
        let user = await db
          .collection('user_cred')
          .findOne({ email: credentials.email });
        if (!user) {
          console.log('해당 이메일은 없음');
          return null;
        }
        const pwcheck = await bcrypt.compare(
          credentials.password,
          user.password
        );
        if (!pwcheck) {
          console.log('비번틀림');
          return null;
        }
        return user;
      },
    }),
  ],

  //3. jwt 써놔야 잘됩니다 + jwt 만료일설정
  session: {
    strategy: 'jwt',
    maxAge: 30 * 24 * 60 * 60, //30일
  },

  callbacks: {
    //4. jwt 만들 때 실행되는 코드
    //user변수는 DB의 유저정보담겨있고 token.user에 뭐 저장하면 jwt에 들어갑니다.
    jwt: async ({ token, user }) => {
      if (user) {
        token.user = {};
        token.user.name = user.name;
        token.user.email = user.email;
      }
      return token;
    },
    //5. 유저 세션이 조회될 때 마다 실행되는 코드
    // 컴포넌트 안에서 보여줄 유저 정보, 토큰에있는 유저들을 컴포넌트 안에서 보여준다고 생각하면됨
    session: async ({ session, token }) => {
      session.user = token.user;
      return session;
    },
  },

  adapter: MongoDBAdapter(connectDB),
  secret: 'qwer1234',
};
export default NextAuth(authOptions);

라이브러리 사용법이라서 외우고 공부할 필요X
그대로 복붙해서 사용하고 어떤 용드의 코드인지만 잘 살펴볼것
주의깊게 봐야할건 2번이다. 로그인시 아이디/비번을 DB에 저장된 내용과 비교하는건 내가 직접 해야하니깐, 나의 DB 상황에 맞게 수정해서 쓰면 되겠다.

회원기능 만들기 OAuth + session 방식

|

Next-auth 라이브러리를 사용하면 기본적으로 모든 방식이 JWT이다.
session방식으로 회원기능을 만들려면 어떻게해야할까?

DB adapter

가입된 유저정보를 DB에 저장하는게 필요하거나, 유저 로그인상태를 엄격하게 관리하고 싶을 때 사용

DB adapter의 동작방식은 다음과 같다.

  1. 첫 로그인시 자동으로 유저를 회원가입 시켜서 DB에 유저 회원정보를 보관해준다.

  2. 로그인시 자동으로 유저가 언제 로그인했는지 세션정보를 DB에 보관해준다.

  3. 서버에서 지금 로그인된 유저정보가 필요하면 JWT가 아니라 DB에 있던 세션정보를 조회해서 가져온다.

  4. 로그아웃시 유저 세션정보는 DB에서 삭제된다.

MongoDB adapter 설정하기

터미널 열고 설치

npm install @next-auth/mongodb-adapter

MongoDB말고 다른 DB에 유저 세션을 저장하고싶다면, 다른 DB adapter를 찾아서 사용하면된다.
참고로 redis같은 DB는 데이터 저장시 하드말고 램을 사용하기 때문에 빨라서 세션 방식 구현할 때 인기가있다고 한다.

/pages/api/auth/[…nextauth].js을 다음과 같이 변경해준다.
(주석처리로 //추가 이부분을 주의깊게 보면 될듯)

import { connectDB } from "@/util/database"; //추가
import { MongoDBAdapter } from "@next-auth/mongodb-adapter"; // 추가

import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";

export const authOptions = {
  providers: [
    GithubProvider({
      clientId: 'Github에서발급받은ID',
      clientSecret: 'Github에서발급받은Secret',
    }),
  ],
  secret : '어쩌구'
  adapter : MongoDBAdapter(connectDB), //추가함
};

export default NextAuth(authOptions);

참고로 connectDB는 DB접속용 코드를 모아놓은 파일이다.

// connectDB
import { MongoClient } from 'mongodb';

const url = 'db주소입력';
const options = { useNewUrlParser: true };
let connectDB;

if (process.env.NODE_ENV === 'development') {
  if (!global._mongo) {
    global._mongo = new MongoClient(url, options).connect();
  }
  connectDB = global._mongo;
} else {
  connectDB = new MongoClient(url, options).connect();
}
export { connectDB };

이렇게 하면 세션으로 회원기능 만들기는 끝이다.

mongoDB 확인해보기

이미지

▲ 로그인후, MongoDB를 확인해보면 이미지와 같이 세개의 컬렉션이 생성되어있다.

sessions: 현재 로그인된 유저정보와 로그인 유효기간이 적혀있다.
users: 유저들 보관하는곳이다. 유저끼리 구분은 이메일로 한다.
accounts: 유저계정 보관하는 곳이다.

하나의 유저는 여러개의 계정을 가지고 있을 수 있기 때문에 여러 계정마다 이메일이 중복될 수도 있다.

Q. users vs accounts 차이

어떤 한 유저가 Github으로도 가입하고 Google로도 가입 해버렸다고 가정해보자.
근데 이사람의 깃헙 계정과 구글 계정이 test@naver.com으로 중복되어있는 경우,

users 컬렉션에는 test@naver.com 이메일을 가진 document 한개만 생성되는데,

accounts 컬렉션에는
test@naver.com + Github이 써있는 document,
test@naver.com + Google이 써있는 document,
이렇게 두개가 생성이 된다.

이런 식으로 유저는 한명이지만, 계정은 두개이상 생성이 가능하다.
(이메일이 같으면 같은 유저라고 자동으로 간주한다.)

DB adapter 컬렉션 저장 위치 변경

DB adapter 사용시 기본으로 설정되어있는 database에 accounts, sessions, users 3개 컬렉션들을 생성한다.

이미지

그래서 forum database에 저런 컬렉션들이 생성된다.

그게 싫고 다른 이름의 database에 넣고 싶으면,
(forum이름의 데이터베이스 안에 집어넣고싶으면)

MongoDB 접속 URL에 물음표가 있을텐데 그거 왼쪽에 원하는 database 이름을 넣으면 된다.

Ex) mongodb+srv://'디비아이디':'디비비번'@cluster0.ip8dsz4.mongodb.net/'이곳에 원하는 db 이름 입력'test