Axios는 브라우저와 Node.js 환경 모두에서 사용할 수 있는 HTTP 비동기 통신 라이브러리입니다. REST API 호출을 위한 비동기 요청을 보다 간편하게 만들 수 있도록 도와주며, 주로 React, Vue와 같은 프론트엔드 프레임워크에서 API 통신을 위해 많이 사용됩니다. 간결한 코드와 높은 유연성으로 많은 개발자들에게 인기가 있습니다.

 

Axios의 주요 특징

  1. Promise 기반: Axios는 Promise를 반환하므로, .then()과 .catch()를 이용해 비동기 작업을 처리할 수 있으며, async/await 구문과도 호환되어 코드 가독성을 높일 수 있습니다.
  2. 요청과 응답 인터셉터 제공: 요청 또는 응답이 완료되기 전에 특정 작업을 수행할 수 있는 인터셉터(interceptors)를 제공합니다. 이를 통해 헤더 설정, 에러 처리 로직, 로딩 스피너 표시 등을 쉽게 구현할 수 있습니다.
  3. 자동 JSON 데이터 변환: 기본적으로 JSON 데이터를 자동으로 직렬화 및 역직렬화해 주어 추가적인 데이터 변환 작업을 줄여 줍니다.
  4. 브라우저와 Node.js 환경에서 모두 사용 가능: 브라우저뿐만 아니라 Node.js 환경에서도 동일한 API로 요청을 보낼 수 있으며, 이는 서버 사이드 렌더링(SSR) 애플리케이션에서도 유용하게 사용될 수 있습니다.
  5. 요청 취소 기능: axios.CancelToken을 사용해 특정 요청을 취소할 수 있으며, 이를 통해 불필요한 네트워크 리소스 소비를 줄일 수 있습니다.

프로미스(promise)는 뭔데?

https://javascript.info/promise-basics

 

Promise

 

javascript.info

 

기본 사용 예시

import axios from 'axios';

// GET 요청 예시
axios.get('/api/data')
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error("Error fetching data:", error);
  });
  
// POST 요청 예시
axios.post('/api/user', { name: 'John', age: 30 })
  .then(response => {
    console.log("User created:", response.data);
  })
  .catch(error => {
    console.error("Error creating user:", error);
  });

 

고급 기능

1. 요청과 응답 인터셉터

인터셉터를 통해 모든 요청과 응답에 대해 미리 정의된 로직을 적용할 수 있습니다. 예를 들어, 모든 요청에 인증 토큰을 추가하거나, 모든 응답 에러를 공통적으로 처리하는 작업을 구현할 수 있습니다.

axios.interceptors.request.use(config => {
  config.headers.Authorization = `Bearer ${localStorage.getItem('token')}`;
  return config;
}, error => {
  return Promise.reject(error);
});

2. 요청 취소

특정 상황에서 요청을 취소할 수 있습니다. 예를 들어, 사용자가 빠르게 입력하거나 검색을 취소할 때, 이전 요청을 취소해 네트워크 자원을 절약할 수 있습니다.

const source = axios.CancelToken.source();

axios.get('/api/data', { cancelToken: source.token })
  .catch(error => {
    if (axios.isCancel(error)) {
      console.log('Request canceled', error.message);
    }
  });

// 요청 취소
source.cancel('Operation canceled by the user.');

3. 요청 Config 옵션

Axios는 다양한 요청 설정 옵션을 지원하여 유연한 API 요청을 가능하게 합니다.

axios({
  method: 'post',
  url: '/api/user',
  data: { name: 'John', age: 30 },
  timeout: 5000, // 타임아웃 설정
  headers: { 'X-Custom-Header': 'customValue' } // 헤더 추가
});

 

Axios와 Fetch의 차이점

  • 호환성: Axios는 구형 브라우저에서 폴리필 없이 바로 사용할 수 있지만, Fetch는 폴리필을 필요로 합니다.
  • 응답 처리 방식: Axios는 기본적으로 JSON 형식을 처리하지만, Fetch는 .json() 메서드를 호출해 수동으로 JSON 형식으로 변환해야 합니다.
  • 에러 처리: Axios는 HTTP 상태 코드가 400 이상일 때 자동으로 에러로 처리되지만, Fetch는 수동으로 에러를 확인해야 합니다.

시간이 지날 수록 학습량이 많아지다보니 이해하지 못하고 넘어가는 부분들도 많아지는 것 같다 

아무리 급해도 제대로 이해하고 그 부분에서 어떤 기술이 왜 어떤 방식으로 쓰였는지에 대해서 더 자세히 알아 가는 과정들이 필요할 것 같다

그러니 차근차근 천천히 학습을 진행해보자 

 

이번에 학습한 내용들은 본격적으로 로그인을 하고나서 서비스가 운영되는 페이지의 사이드바를 구현하는 것이 주요한 목적이었다. 

과거에 이러한 방식은 간단하게 구현할 수 있었는데 이 프로젝트에서는 다양한 기술들이 사용되다보니 어려움을 느꼈다. 

 

AuthContext

먼저 AuthContext.tsx파일을 만들어서 인증 기능을 추가했다. 

이 파일에서 사용된 SessionProvider는 next-auth의 세션 관리를 제공하는 역할을 한다. 또한 현재 사용자의 전체 앱에서 사용할 수 있도록 만들어 줄 수 있다. 따라서 이 파일은 인증 상태를 전역으로 제공하여 하위 컴포넌트들이 인증 정보를 사용할 수 있도록 지원하는 컨텍스트 역할을 하는 것이다.

export default function AuthContext({ children }: AuthContextProps) {
  return <SessionProvider>{children}</SessionProvider>;
}

 

그리고 이에 대한 해당 내용을 밖에 있는 layout.tsx 에 추가해서 ToasterContext를 감싸준다. 

<AuthContext>
  <ToasterContext />
  {children}
</AuthContext>

이러한 이유는 알림기능을 전역에서 관리하기 위해 ToasterContext를 AuthContext와 함께 상위 컴포넌트에서 제공하기 위함이다. 이 설정은 메시지 알림을 사용하는 하위 컴포넌트들에게 손쉽게 전급할 수 있게 해주고, 알림의 상태와 생성을 전역에서 일관되게 관리할 수 있또록 도와준다. 

 

useSession과 useRoute

1. useSession

useSession은 NextAuth에서 제공하는 훅으로 현재 사용자의 세션 정보를 가져와 상태를 확인할 수 있다. 여기서는 session객체를 통해 사용자의 인증 상태를 확인한다. session.status가 "authenticated"일 경우 사용자가 로그인된 상태임을 의미하며, 이 상태가 되면 사용자 페이지로 이동하게 된다. 

const session = useSession();
useEffect(() => {
  if (session?.status === "authenticated") {
    router.push("/users");
  }
}, [session?.status, router]);

 

2. useRouter

useRouter는 Next.js의 내장 라우팅 훅으로, 클라이언트 측에서 페이지 이동이나 라우팅을 제어할 수 있게 한다.

여기서는 세션 상태에 따라 users페이지로 리다이렉트하는 데 사용된다. 사용자가 로그인에 성공하거나 소셜로그인을 통해 인증되면, /users로 이동하여 해당 사용자가 볼 수 있는 메인 페이지로 안내된다. 

const router = useRouter();
router.push("/users");

아참 자동으로 불러오면 next/router의 useRouter가 불러와지는데 최신 버전은 next/navigation이므로 주의해야한다. 

 

회원 가입 후 바로 로그인 

const onSubmit: SubmitHandler<FieldValues> = (data) => {
    setIsLoading(true);
    if (variant === "REGISTER") {
      axios
        .post("/api/register", data)
        .then(() => signIn("credentials", data))
        .catch(() => toast.error("Something went wrong"))
        .finally(() => setIsLoading(false));
    }

이번 시간에서는 사용자 경험을 개선한 것이 있었는데 ".then(()=>signIn("credentials",data))"구문을 추가했다. 

이것은 회원가입을 한 후 다시 로그인 페이지로 돌아가서 로그인을 하는 것이 아니라 회원가입을 했을 때 바로 로그인이 되어 페이지가 넘어갈 수 있도록 하였다.

 

미들웨어 

middleware.ts파일은 Next.js의 미들웨어 기능을 활용해 특정 페이지에 대한 접근 권한을 관리하는 역할을 한다. next-auth의 withAuth함수를 사용하여 사용자가 인증되지 않은 경우 로그인 페이지로 리디렉션하도록 설정한다.

1. withAuth

- withAuth는 next-auth라이브러리에서 제공하는 인증 미들웨어로, 사용자가 특정 페이지에 접근할 때 인증 상태를 확인하여 인증되지 않은 사용자는 지정된 페이지로 리디렉션할 수 있도록 설정해준다. 

- withAuth함수는 미들웨어로서 특정 경로에 대한 요청을 가로채고 인증된 사용자만 접근을 허용하는 역할을 한다.

2. 페이지 리디렉션 설정 

pages: {
  signIn: "/",
},

- pages객체에서 signIn속성을 사용하여 인증되지 않은 사용자가 접근할 경우 리디렉션할 페이지를 지정한다. 여기서 signIn:"/"로 설정하였기 때문에 사용자가 로그인이 필요한 페이지에 접근하려고 하면 / 경로로 이동된다. 

- 이는 일반적으로 로그인 페이지를 의미하며, 사용자가 인증 없이 접근하려고 할 때 로그인을 유도하는 역할을 한다. 

3. config설정

export const config = {
  matcher: ["/users/:path*"],
};

- config객체에서 matcher속성을 사용하여 미들웨어로 적용할 경로  패턴을 지정한다. 

- "/users/:path*"는 users/뒤에 오는 모든 하위 경로를 포함하므로, /users, /users/profile, /users/setting등 /users로 시작하는 모든 경로가 미들웨어의 적용대상이된다. 

- 따라서 /users관련 페이지에 접근하려는 사용자가 인증되지 않았다면 , /로그인 페이지로 리디렉션된다. 

 

이 미들웨어가 작용되는 흐름을 살펴보면 사용자가 /user로 시작하는 페이지에 접근하레되면 이 미들웨어가 실행되고 withAuth가 사용자 인증 상태를 확인하고 인증된 경우에는 정상작동 아니면 signIn에서 지정한 "/"경로로 리디렉션되어 로그인 페이지로 이동. 로그인 후 사용자는 인증 상태에 따라 /users 페이지에 다시 접근 가능 후후 ! 

 

UserPage

이제 본격적으로 Userpage를 만들어보자 

app/users라는 폴더를 새롭게 만들어서 그 안에 layout.tsx와 pages.tsx를 마련한다. 

page.tsx에는 <Emptystate/>라는 컴포넌트를 만들어서 빈공간을 채워준다. 

import Emptystate from "../components/EmptyState";

const Users = () => {
  return (
    <div className="hidden lg:block lg:pl-80 h-full">
      <Emptystate />
    </div>
  );
};
export default Users;

<emptystate>는 특별한 건 없으니깐 굳이 설명은 안하겠음 

 

그리고 user폴더 안에 layout.tsx를 만든다. 

layout은 컴포넌트는 해당 경로 아래에 있는 페이지들의 기본구조로 사용된다. 이 코드에서는 Sidebar컴포넌트를 사용해 사이드바 메뉴와 메인컨텐츠(children)를 감싸는 레이아웃을 만들어준다.

import Sidebar from "../components/sidebar/Sidebar";

export default async function UserLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    // @ts-expect-error Server Component
    <Sidebar>
      <div className="h-full">{children}</div>
    </Sidebar>
  );
}

 

userlayout

- 이 컴포넌트는 비동기 함수로 정의되어 있다. next.js에서는 레이아웃이나 페이지에서 데이터를 서버에서 미리 가져오는 기능을 지원하는데, 이 비동기 함수가 그 목적을 위해 비동기 처리를 사용할 수있다. 

- userlayout은 기본적으로 모든 user페이지의 레이아웃으로 사용된다. 이 파일의 기능은 export default로 내보내지므로, 해당 경로에 접근할 때 자동으로 이 레이아웃이 적용된다. 

children 속성

- children속성은 이 레이아웃을 사용하는 페이지가 렌더링하는 콘텐츠를 의미한다. next.js에서는 layout.tsx파일이 해당 경로의 모든 자식 페이지의 커테이너가 되며, 자식 페이지들이 children으로 전달된다. 

 

- @ts-expect-error Server Component이 주석은 타입스크립트 컴파일러에게 특정 에러를 무시하라고 지시한다. 

- server component관련 에러를 무시하도록 설정되어 있는데 이는 넥스트13버전에서 도입된 기능으로, 서버 컴포넌트와 클라이언트 컴포넌트를 구분해 렌더링 효율을 높일 수 있다.

 

Hooks

본격적으로 사이드바를 구성하기 전에 커스텀훅을 만들었다. 

app/hooks폴더를 만들어서 그 안에 useConversation.ts와 UseRoutes.ts를 구성하였다. 

 

useConversation

useConversation은 특정대화에 대한 ID를 추출하고 대화가 열려있는지 여부를 확인하는 커스텀훅이다. 이 훅은 useParams를 통해 URL의 파라미터에서 대화 ID를 얻고, 해당ID가 있는지 여부를 통해 대화가 열려있는지를 확인한다. 

 

1. const params = useParams()

- useParams는 next.js의 next/navigation에서 제공하는 훅으로, URL의 파라미터 값을 가져온다. 이 경우 URL에 conversationId라는 파라미터가 있다면 이를 가져온다. 

- 예를 들어 URL이 /conversation/123이면, params.conversationId는 123이된다.

2. conversationId변수 생성

const conversationId=useMemo(()=>{...}, [params?.conversationId]);

- conversationId는 params에서 conversationId가 존재하는지 확인하고, 존재하지 않는다면 빈 문자열 "" 를 반환한다.

- useMemo를 사용해 params?.conversationID가 변경될 때만 conversationId를 새로 계산하여 최적화한다.

- 최종적으로 conversationId는 string타입으로 강제된다.

3. isOpen변수 생성 

const isOpen = useMemo(()=>!!conversationId, [conversationId]);

- isOpen은 대화가 열려있는 지 확인하는 변수. conversationId가 빈 문자열이 아니라면 isOpen은 true가 된다.

- !!conversationId를 사용하여 conversationId가 존재할때 true, 없을 때 false로 변환한다.

4. 변환값 최적화 

return useMemo(() => ({ isOpen, conversationId }), [isOpen, conversationId]);

- 최종적으로 isOpen과 conversationId를 객체로 묶어 반환한다. 

- useMemo를 사용해 반환값이 isOpen이나 conversationId가 변경될 때만 새 객체로 계산되도록 하여 불필요한 재렌더링을 방지한다.

useRoute

useRoute는 여러 페이지나 기능(예:채팅, 사용자 목록, 로그아웃)에 대한 라우트 데이터를 생성하는 커스텀 훅이다. 각 라우트는 경로, 아이콘, 활성상태 및 클릭 시 동작을 포함하며, 이를 통해 사용자가 현재 위치에 따라 동적으로 활성화된 상태를 표시할 수 있다.

 

1. 현재 경로 및 대화 ID 가져오기:

  • const pathname = usePathname();: 현재 URL 경로를 가져옵니다. 이 경로는 사용자가 어디에 있는지를 확인하여 해당 라우트를 활성화하는 데 사용됩니다.
  • const { conversationId } = useConversation();: useConversation 훅을 통해 현재 대화의 ID를 가져옵니다. conversationId가 존재하면 Chat 라우트를 활성화하는 데 사용됩니다.

2. routes 배열 생성:

  • useMemo를 사용해 routes 배열을 구성하여 성능을 최적화합니다. pathname이나 conversationId가 변경될 때만 배열이 다시 생성됩니다.
  • routes 배열에는 각 페이지나 기능에 대한 정보가 포함됩니다.

각 라우트 객체에는 다음 속성이 있습니다:

  • label: 라우트의 이름 또는 설명 텍스트입니다. 예를 들어 "Chat", "Users", "Logout" 등으로 설정되어 있습니다.
  • href: 라우트의 경로입니다.
    • "Chat": /conversation
    • "Users": /users
    • "Logout": 로그아웃 버튼으로, href는 "#"로 설정되어 있어 페이지 이동이 아닌 클릭 시 로그아웃을 수행합니다.
  • icon: react-icons 라이브러리에서 가져온 아이콘으로, 각각의 라우트와 시각적으로 연관됩니다.
    • HiChat은 "Chat" 아이콘, HiUsers는 "Users" 아이콘, HiArrowLeftOnRectangle는 "Logout" 아이콘으로 지정되어 있습니다.
  • active: 현재 라우트가 활성화 상태인지 나타내는 속성으로, 이를 통해 사용자가 현재 위치에 따라 해당 라우트를 강조할 수 있습니다.
    • "Chat": 현재 pathname이 /conversations이거나 conversationId가 존재할 때 활성화됩니다.
    • "Users": 현재 pathname이 /users일 때 활성화됩니다.

3. 로그아웃 라우트의 onClick 이벤트:

  • "Logout" 라우트에는 onClick 속성이 추가되어 있어 클릭 시 signOut()을 호출해 로그아웃이 수행됩니다.

4. 훅의 반환값:

  • routes 배열을 반환하여 다른 컴포넌트에서 사용될 수 있도록 합니다. 이 배열을 통해 현재 위치에 따라 라우트가 동적으로 활성화되거나 비활성화된 상태로 렌더링될 수 있습니다.

SideBar

위에서 보여진것 처럼 User페이지에서 Sidebar는 레이아웃으로 고정되어 있는 형태로 나타나게 된다. 

컴포넌트에 Sidebar 폴더를 만들어서 그 안에 sidebar를 구성하는 여러 컴포넌트들을 만들었다. 

async function Sidebar({ children }: { children: React.ReactNode }) {
  const currentUser = await getCurrentUser();
  return (
    <div className="h-full">
      <DesktopSidebar currentUser={currentUser!} />
      <MobileFooter />
      <main className="lg:pl-20 h-full">{children}</main>
    </div>
  );
}

- Sidebar는 비동기 함수로 사용자 정보를 받아와 컴포넌트에 전달하고 화면 구조를 정의한다. 

- children속성에는 메인 콘텐츠가 들어가면, 이 콘텐츠는 사이드바와 푸터 외에 Sidebar내에서 렌더링 된다.

- 나중에 화면의 동적 움직임에 따라서 <DesktopSidebar>와 <MobileFooter>가 움직이게 된다.

 

getCurrentUser

sidebar에서 사용된 getCurrentUser는 현재 로그인한 사용자의 세션 정보를 확인하고, 해당 사용자의 상세 정보를 데이터베이스에서 가져오는 역할을 한다. 이 함수는 사용자가 로그인된 상태인지 확인하고, 로그인된 상태라면 데이터베이스에서 이메일을 기반으로 해당 사용자의 정보를 조회하여 반환한다. 주요 기능은 prisma와 getSession을 활용해 현재 사용자를 식별하고 정보를 가져온다.

 

세션정보 확인 

const session = await getSession();
if (!session?.user?.email) {
  return null;
}

- getsession을 호출하여 사용자 세션 정보를 session변수에 할당한다. 

- 세션 객체가 없거나 user정보 안에 email이 없다면 로그인 하지 않은 상태로 간주하고 null을 반환한다. 

 

사용자 조회 

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

 

- session.user.email을 통해 데이터베이스에서 해당 이메일을 가진 사용자를 조회한다. 

- findUnique메소드는 where절을 사용하여 특정 조건을 만족하는 한 개의 레코드를 검색한다. 

- 이메일을 기반으로 사용자를 찾는 방식이므로, 이메일이 데이터베이스에 없을 경우 null을 반환한다. 

 

오류 처리

catch (error: any) {
  return null;
}

- 이 하뭇는 오류가 발생할 경우 null을 반환하도록 처리되어 있어, 에러 상황에서도 null을 반환함으로써 프로그램이 중단되지 않게 한다.

DesktopSidebar

데스크탑 사이드바를 만들어보자 

1. 첫번째 네비게이션

<ul role="list" className="flex flex-col items-center space-y-1">
    {routes.map((item) => (
      <DesktopItem
        key={item.label}
        href={item.href}
        label={item.label}
        icon={item.icon}
        active={item.active}
        onClick={item.onClick}
      />
    ))}
  </ul>

- routes에 담긴 각 메뉴 항목을 순회하면서 DesktopItem컴포넌트를 렌더링한다. 

- key, href, label, icon, active, onClick등의 속성을 전달해 개별 메뉴 항목을 정의한다. 

- active속성은 현재 페이지 위치와 일치하는 메뉴에 강조 표시를 해준다. 

 

2. 두번째 네비게이션 (아바타)

<div
    onClick={() => setIsOpen(true)}
    className="cursor-pointer hover:opacity-75 transition"
  >
    <Avatar user={currentUser} />
  </div>

- 사이드바 하단에는 사용자의 Avatar컴포넌트를 표시하며, currentUser를 전달해 사용자의 프로필 이미지를 렌더링한다. 

- 아바트를 클릭시 isOpen상태를 true로 변경해 추가적인 사용자 인터페이스를 열 수 있게 한다. 

- hover:opacity-75로 마우스 오버 시 살짝 투명해지는 효과가 작동한다. 

 

MobileFooter

이 컴포넌트는 모바일 화면 하단에 고정된 푸터 네비게이션을 렌더링 하며 useRoutes와 useConversation훅을 사용해 네비게이션 항목과 현재 대화 상태를 기반으로 동작을 결정한다.  사용자가 현재 대화중이라면 푸터 네비게이션을 숨긴다. 

const MobileFooter = () => {
  const routes = useRoutes();
  const { isOpen } = useConversation();
  if (isOpen) {
    return null;
  }

routes와 isOpen상태 가져오기 

- routes: useRoutes훅에서 반환된 메뉴 항목 배열이다. href, active, icon, onClick등 필요한 정보가 포함되어 있다. 

- isOpen: 현재 대화 창이 열려있는지 여부를 나타낸다. is Open이 true이면 푸터를 렌더링하지 않고 null을 반환해 숨긴다. 

 

오늘은 내일 일정 때문에 ... 

Login기능을 만들어 보았다. 

toast기능

먼저 토스트 기능을 만들어 보았다. 다음과 같은 방법으로 설치하자.

npm install react-hot-toast

 

 

이후 app/context/ToasterContext.tsx에 파일을 구성한다.

해당 라이브러리는 토스트 기능을 간편하게 사용할 수 있는 라이브러리이다. 

"use client";

import { Toaster } from "react-hot-toast";

const ToasterContext = () => {
  return <Toaster />;
};

export default ToasterContext;

 

 

그리고 layout.tsx에서 ToasterContext를 추가해준다. 

 <html lang="en">
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <ToasterContext />
        {children}
      </body>
    </html>

 

그리고 AuthForm.tsx에 있는 register에 toast를 추가 

.post("/api/register", data)
        .catch(() => toast.error("Something went wrong"))
        .finally(() => setIsLoading(fal

Register버튼을 눌렀을 때 오류가 발생하면 해당 메시지가 pop up하게 된다.

 

LOGIN기능

토스트도 만들었으니 Login기능을 마련해보자 

app/(site)/component/AuthForm.tsx에 onSubmit함수 안의 내용이다. 

if (variant === "LOGIN") {
      signIn("credentials", {
        ...data,
        redirect: false,
      })
        .then((callback) => {
          if (callback?.error) {
            toast.error("Invalid credentials");
          }
          if (callback?.ok) {
            toast.success("Logged in!");
          }
        })
        .finally(() => setIsLoading(false));
    }
  • variant === "LOGIN": variant가 "LOGIN"일 때만 로그인 로직을 실행합니다.
  • signIn("credentials", { ...data, redirect: false }): NextAuth의 signIn 함수를 사용해 사용자 자격 증명(이메일, 비밀번호)으로 로그인 요청을 보낸다. redirect: false는 로그인 후 페이지 리디렉션을 막는다.
  • .then((callback) => { ... }): signIn 함수가 성공적으로 실행되면 callback 객체가 반환된다.
    • callback?.error: 로그인 실패 시, toast.error로 "Invalid credentials" 메시지를 표시한다.
    • callback?.ok: 로그인 성공 시, toast.success로 "Logged in!" 메시지를 띄운다.
  • .finally(() => setIsLoading(false)): 로그인 시도가 끝나면 로딩 상태를 해제한다.

그럼 로그인 기능은 완료다

 

Social Login

내가 만드는 로그인에서 소셜로그인은 깃허브와 구글이다. 이들을 사용하려면 깃허브와 구글에서 OAUTH기능을 요청해서 ID와 비밀번호를 받아와야한다. 

 

그 과정은 (생략) 받아온 내용은 .env에서 GITHUB_ID, GITHUB_SECRET, GOOGLE_CLIENT_ID,GOOGLE_CLIENT_ID에 받아온 정보를 입력해주면 사용할 수있다. 

const socialAction = (action: string) => {
    setIsLoading(true);

    signIn(action, { redirect: false })
      .then((callback) => {
        if (callback?.error) {
          toast.error("Invalid Credentials");
        }
        if (callback?.ok && !callback?.error) {
          toast.success("Logged in!");
        }
      })
      .finally(() => setIsLoading(false));
  };

 

이건 위에서 정리된 내용과 비슷하기 때문에 설명은 생략 

Register기능을 만들어 보았다.

Register.ts

1. bcrypt 및 prisma 모듈 불러오기

import bcrypt from "bcrypt";
import prisma from "@/app/libs/prismadb";
import { NextResponse } from "next/server";

- bcrypt: 비밀번호를 안전하게 해싱하는데 사용되는 모듈이다. 해싱은 원본 비밀번호를 복구할 수 없도록 암호화하는 과정으로 보안상 중요한 부분이다. 

- prisma : Prisma클라이언트를 가져와 데이터베이스와 상호작용할 수 있게 한다. 이 설정 파일은 @/app/libs/prismadb에 위치해 있다.

- NextResponse: Next.js에서 제공하는 응답 객체로, API route의 응답을 더 쉽게 생성할 수 있다. 

 

2. Post핸들러 정의 

export async function POST(request: Request) { ... }

Post함수는 HTTP POST요청을 처리한다. Next.js의 API Routes는 함수형 엔드포인트이므로, POST함수 이름을 통해 어떤 요청 방식인지를 알 수 있다. 

 

3. 요청 본문 파싱 및 필드 검증

const body = await request.json();
const { email, name, password } = body;

if (!email || !name || !password) {
  return new NextResponse("Missing info", { status: 400 });
}

- request.json(): 클라이언트 요청에서 JSON데이터를 추출한다. 

- const { email, name, password } =body : 요청에서 이메일, 이름, 비밀버호를 구조 분해 할당으로 추출한다. 

 

※구조 분해할당은 아래 문설를 참고하자https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

 

구조 분해 할당 - JavaScript | MDN

구조 분해 할당 구문은 배열이나 객체의 속성을 해체하여 그 값을 개별 변수에 담을 수 있게 하는 JavaScript 표현식입니다.

developer.mozilla.org

- if (!email || !name || !password):필수 입력값들이 모두 있는지 확인한다. 누락된 값이 있다면, "Missing info"라는 메시지와 함께 상태코드 400(Bad Request)을 반환해 클라이언트에 오류를 알린다. 

 

4. 비밀번호 해싱 

const hashedPassword = await bcrypt.hash(password, 12);

- bcrypt.hash(password, 12): 입력받은 비밀번호를 해싱한다. 두번째 인자 12는 솔트라운드 값으로, 숫자가 높을 수록 보안성은 증가하지만 속도는 느려진다. 

- 해싱된 비밀번호는 데이터베이스에 저장되므로, 원본 비밀번호를직접 저장하지 않아도 보안이 보장된다. 

 

5. 사용자 생성 

const user = await prisma.user.create({
  data: {
    email,
    name,
    hashedPassword,
  },
});

- prisma.user.create() : Prisma ORM을 사용해 User테이블에 새로운 사용자를 추가한다. email, name, hashedPassword를 data로 전달하여 데이터베이스에 저장한다. 

- Prisma는 스키마 정의에 따라 User테이블 구조를 이해하고, 필요한 SQL쿼리를 자동으로 생성해 데이터 베이스와 상호작용한다. 

 

6. 성공응답반환 

return NextResponse.json(user);

- NextResponse.json(user): 생성된 사용자 객체를 JSON형식으로 응답한다. 클라이언트는 새로 생성된 사용자 데이터를 응답으로 받아 확인할 수 있다.

 

7. 에러 핸들링 

} catch (error: any) {
  console.log(error, "REGISTRATION_ERROR");
  return new NextResponse("Internal Error", { status: 500 });
}

- try-catch문 : 코드 실행 중 오류가 발생할 경우, catch 블록으로 이동한다. 

- console.log(error,"REGISTRATION_ERROR"): 콘솔에 오류 내용을 출력해 개발자가 원인을 파악할 수 있게 한다. 

- return new NextResponse("Internal Error", {status:500}): 클라이언트에게 500상태와 함께 오류 메시지를 반환한다. 

 

Authform에 추가 

이전에 Authform.tsx의 onSubmit함수에서 조건문의 비어있던 내용을  채워줘야 한다. 

if (variant === "REGISTER") {
      axios
        .post("/api/register", data)
        .catch(() => toast.error("Something went wrong"))
        .finally(() => setIsLoading(false));
    }

 

axios를 사용하기 위해서 이를 설치해주었다.

npm install axios

 

axios란?

브라우저와 Node.js 환경에서 모두 사용할 수 있는 HTTP 클라이언트 라이브러리이다. 주로 API 서버와 통신할 때 사용되며, 간단하고 직관적인 문법으로 GET, POST, PUT, DELETE 등의 HTTP 요청을 보낼 수 있다.

 

- .post("/api/register",data): axios라이브러리를 통해 /api/register엔드포인트에 POST요청을 보낸다. data객체에는 사용자의 이름, 이메일 ,비밀번호가 포함된다. 

- .catch(() => toast.error("Something went wrong")): 요청이 실패하면, toast.error로 "Something went wrong" 메시지를 띄워 사용자가 오류를 알 수 있게 한다.

- .finally(() => setIsLoading(false)): 요청 완료 후에는 성공 여부와 상관없이 로딩 상태를 해제한다.

 

그리고 화면 상에서 회원가입을 한후 register를 누르고 개발자 도구의 network탭에서 register의 payload를 확인해볼 수 있다. 

 

다음과 같이 요청이 잘 되고 있다. 

 

Prisma에 대한 설정이 다 끝난후 NextAuth에 대해서 진행을 합니다.

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

 

 

1. next-auth@latest: Next.js 애플리케이션에서 인증 기능을 제공하는 next-auth의 최신 버전입니다.

2. @prisma/client: Prisma 클라이언트로, 데이터베이스와 통신하고 데이터를 조회 및 수정할 수 있습니다

3. @next-auth/prisma-adapter: NextAuth와 Prisma 간의 연동을 돕는 어댑터입니다.

4. bcrypt: 비밀번호를 해시하고 비교하는 데 사용되는 암호화 라이브러리입니다.

 

이후 추가로 설치해줍니다ㅏ.

npm install -D @types/bcrypt

 

이는 bcrypt라이브러리의 타입 정의 파일을 설치하는 명령입니다. @types/bcrypt는 typescript환경에서 bcrypt의 타입을 명확하게 정의하여 typescript의 타입검사 기능을 지원합니다. 

 

PrismaClient인스턴스 생성 

app에 lib이라는 폴더를 만들고 그 안에 prismadb.ts를 만들어줍니다. 이 파일에서는 Prismaclient인스턴스를 생성하고 관리하여, 프로젝트 내에서 Prisma ORM을 효율적으로 사용할 수 있도록 설정하는 역할을 합니다. 특히 Next.js환경에서 중요한 여러요소들을 포함합니다.

 

1. 

declare global {
  var prisma: PrismaClient | undefined;
}
  • TypeScript의 global 선언을 사용하여 전역에서 prisma 변수를 선언합니다. 이는 Hot Reloading 중복 생성을 피하는 중요한 단계입니다.
  • 이 전역 선언은 prisma가 undefined이거나 PrismaClient 객체일 수 있도록 설정하고, Next.js에서 개발 중 Hot Reloading 시 Prisma 인스턴스를 재사용할 수 있게 만듭니다.

2.

const client = globalThis.prisma || new PrismaClient();
  • globalThis.prisma가 이미 존재한다면 기존의 prisma 인스턴스를 사용하고, 그렇지 않다면 새로운 PrismaClient 인스턴스를 생성하여 client에 할당합니다. 이는 데이터베이스 연결을 중복으로 생성하는 것을 방지합니다.
  • globalThis는 전역 객체로, 브라우저와 Node.js 환경에서 모두 동작합니다.

3.

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

 

  • 개발 환경에서는 prisma 인스턴스를 전역 변수인 globalThis.prisma에 할당하여 재사용합니다.
  • Production 환경에서는 Prisma 인스턴스를 매번 새로 생성하는 방식으로 메모리 사용을 절감하고, 개발 환경에서는 Hot Reloading 동안에도 같은 인스턴스를 유지하게 해줍니다.

 

 

authOptions설정 

authOptions는 NextAuth설정의 핵심요소로, 인증 과정과 관련된 모든 설정을 담고 있습니다. 

  • adapter: PrismaAdapter(prisma)를 설정하여 NextAuth가 Prisma와 연결되도록합니다. 이를 통해 인증에 필요한 사용자 데이터가 Prisma를 통해 데이터베이스에 저장됩니다.
  • provider: github, Google, Gredential와 같은 여러 인증 제공자를 설정합니다. 
GithubProvider({
      clientId: process.env.GITHUB_ID as string,
      clientSecret: process.env.GITHUB_SECRET as string,
    }),
GoogleProvider({
      clientId: process.env.GOOGLE_ID as string,
      clientSecret: process.env.GOOGLE_SECRET as string,
    }),
  • CredentialsProvider: 이메일과 비밀번호 기반의 인증을 설정합니다. 
    • credentials필드: 사용자가 입력해야 하는 email과 password필드를 정의합니다. 
    • authoize함수: 사용자의 자격 증명을 확인하는 함수입니다. 
      • 이메일 및 비밀번호 유효성 검사: 이메일과 비밀번호가 제공되지 않으면 invalid credentials오류를 발생시킵니다.
      • 데이터베이스 조회: prisma.user.findUnique()를 사용하여 데이터베이스에서 이메일로 사용자를 검색합니다.
      • 비밀번호 확인 : bcrypt.compare()를 통해 사용자가 입력한 비밀번호와 데이터 베이스에 저장된 해시 비밀번호가 일치하는지 확인합니다. 
      • 반환 : 비밀번호가 일치하면 해당 사용자를 반환하고, 일치하지 않으며 오류를 발생시킵니다. 
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

NODE_ENV가 "development"일 때 디버그 모드를 활성화하여 상세한 로그를 출력합니다.

debug: process.env.NODE_ENV === "development",

 

session

JWT기반의 세션을 관리하기 위한 설정 

- strategy: jwt로 설정해 서버에 세션 정보를 저장하지 않고 JWT토큰을 통해 세션을 유지

session: {
    strategy: "jwt",
  },

 

secret

인증에 필요한 비밀 키로, 환경 변수 NEXTAUTH_SECRET에서 가져옵니다.

secret: process.env.NEXTAUTH_SECRET

 

Handler생성 및 내보내기 

NextAuth(authOptions)로 handler를 생성하고 GET 및 POST 메서드로 handler를 내보냅니다. 이를 통해 NextAuth의 인증 API가 GET 및 POST 요청을 통해 접근할 수 있게 됩니다.

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

 

동작 흐름 요약

  1. 사용자가 로그인 요청을 보낼 때 authOptions의 authorize 함수가 실행됩니다.
  2. 이메일-비밀번호 인증의 경우:
    • 입력된 이메일로 데이터베이스에서 사용자를 조회하고, 비밀번호가 일치하는지 확인합니다.
  3. OAuth 인증(GitHub, Google)의 경우:
    • GitHub 또는 Google을 통해 OAuth 인증이 수행됩니다.
  4. 인증 성공 시 JWT 토큰이 생성되고 이를 통해 세션이 유지됩니다.

보안 및 주요 고려사항

  • 비밀번호 보안: bcrypt로 해시된 비밀번호를 데이터베이스에 저장하고, 로그인 시에도 해시 비교를 수행하여 안전하게 인증합니다.
  • JWT 기반 세션: 서버 자원 소모가 적고 효율적이며, 클라이언트 측에서 관리됩니다.
  • 환경 변수 사용: 민감한 데이터(clientId, clientSecret, NEXTAUTH_SECRET)는 환경 변수에서 불러와 보안을 강화합니다.

Prisma 설치 

프리즈마를 설치

npm install -D prisma

 

이후 초기설정 prisma디렉토리를 생성한다.

npx prisma init

그럼 프로젝트 루트에 prisma디렉터리와 schma.prisma파일이 생성된다. 이 파일은 prisma와 데이터베이스 설정을 정의하는 파일이다. 

 

MongoDB데이터베이스 연결 

(1).env파일에 MongoDB URL추가

MongdoDB 데이터베이스를 설정하려면 schema.prisma의 datasource블록을 수정하여 mongoDB와 연결해야한다.

.env파일에서 DATABASE_URL을 설정해준다.

DATABASE_URL="mongodb+srv://username:password@cluster0.mongodb.net/mydatabase?retryWrites=true&w=majority"

(2)schema.prisma파일에서 MongoDB설정

schema.prisma에서 datasource블록의 내용도 수정

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

 

prisma스키마 정의 

Prisma스키마는 schema.prisma파일의 model블록에서 정의된다. MongoDB와 함께 사용할 수 있도록 각 모델에 고유 식별자를 지정해야한다. MongoDB의 경우, 각 모델에 고유 식별자를 지정해야한다. mongoDB의 경우, 각 모델에 @id로 지정된 _id필드를 포함해야한다. 

 

여기서는 User, Account, Conversation, Message모델을 만들었다.

 

User 모델 

 

  • id: MongoDB의 ObjectId로, 사용자 고유 ID. @id와 @default(auto())를 통해 자동 생성됩니다.
  • name, email, image: 각각 사용자의 이름, 이메일, 프로필 이미지입니다. email은 유일하도록 설정됩니다.
  • emailVerified: 이메일 인증 여부를 저장하는 DateTime 필드.
  • hashedPassword: 암호화된 비밀번호.
  • createdAt: 생성 시점, 기본값은 현재 시각.
  • updatedAt: 자동으로 수정 시각 업데이트.
  • conversationsIds, conversations: 사용자와 연결된 대화의 ID 배열과 대화 관계를 나타냅니다.
  • seenMessageIds, seenMessages: 사용자가 본 메시지의 ID 배열과 메시지 관계를 정의합니다.
  • accounts: 사용자의 계정과 연결된 Account 모델 배열.
  • messages: 사용자가 보낸 메시지를 나타내는 Message 모델 배열.

Account 모델

사용자의 계정 정보와 외부 인증을 위한 필드들을 정의

  • id: MongoDB ObjectId로, 계정 고유 ID.
  • userId: User 모델과의 관계를 나타내는 필드. 사용자 삭제 시 연결된 계정도 삭제됩니다 (onDelete: Cascade).
  • type, provider, providerAccounId: 계정 타입, 외부 인증 제공자 이름, 그리고 제공자 내 계정 ID를 나타냅니다.
  • refresh_token, access_token, expires_At, token_type, scope, id_token, session_state: 인증 토큰 관련 필드.
  • user: User 모델과의 관계를 나타내며, userId와 연결되어 있습니다.

Conversation 모델

대화방의 정보를 정의하며, 그룹 채팅 또는 개인 채팅을 모두 포함

  • id: 대화 고유 ID, MongoDB ObjectId.
  • createdAt, lastMessageAt: 대화가 생성된 시점과 마지막 메시지가 전송된 시점을 나타내며, 둘 다 기본값은 현재 시각입니다.
  • name: 대화방 이름, 그룹 채팅에 유용.
  • isGroup: 그룹 대화 여부를 나타내는 불리언 값.
  • messagesIds, messages: 대화와 연결된 메시지 ID 배열과 Message 모델 배열.
  • userIds, users: 대화에 참여 중인 사용자들의 ID 배열과 User 모델 배열.

Message 모델

메시지의 본문, 이미지, 작성 시점 등의 메시지 정보를 정의

  • id: 메시지 고유 ID, MongoDB ObjectId.
  • body, image: 메시지 내용과 첨부 이미지 URL.
  • createdAt: 메시지가 생성된 시점을 나타내며 기본값은 현재 시각.
  • seenIds, seen: 메시지를 본 사용자 ID 배열과 User 모델 배열. Seen이라는 이름으로 관계를 설정합니다.
  • conversationId, conversation: 메시지가 속한 대화방 ID와 Conversation 모델과의 관계.
  • senderId, sender: 메시지 전송자의 ID와 User 모델과의 관계를 정의하며, 사용자가 삭제되면 메시지도 삭제됩니다 (onDelete: Cascade).

 

스키마 파일 데이터베이스 반영 

스키마에서 정의한 모델들을 데이터 베이스에 반영하는 단계를 거쳐야한다. 

npx prisma db push

이 명령어 이후에 mongoDB에서 확인해 보면 만들었던 모델들이 잘 반영되어있던걸 확인할 수 있었다.

 

Prisma는 데이터베이스와 Node.js 애플리케이션 간의 상호작용을 돕는 강력한 ORM(객체 관계 매퍼) 툴입니다. Prisma는 SQL을 직접 작성하지 않고도 데이터베이스 쿼리를 수행할 수 있게 해주며, 코드 내에서 데이터베이스 스키마를 쉽게 정의하고 관리할 수 있습니다. 특히 TypeScript와의 뛰어난 호환성과 타입 안전성을 제공하는 점에서 최근 많은 관심을 받고 있습니다.

 

Prisma의 주요 개념과 구성 요소

Prisma는 세 가지 핵심 도구로 구성됩니다.

  1. Prisma Client:
    • 데이터베이스와의 상호작용을 위한 자동 생성된 타입 쿼리 빌더입니다.
    • Prisma Client는 @prisma/client 패키지를 통해 설치됩니다.
    • 데이터베이스에서 레코드를 조회, 생성, 업데이트, 삭제하는 쿼리를 직관적인 코드로 작성할 수 있습니다.
    • 예를 들어, prisma.user.findMany()와 같은 구문으로 User 테이블의 데이터를 가져올 수 있습니다.
  2. Prisma Migrate:
    • Prisma의 마이그레이션 시스템으로, 데이터베이스 스키마를 버전 관리하고 데이터베이스와 코드 간의 구조를 일치시킵니다.
    • 데이터베이스 스키마가 변경될 때, 마이그레이션 파일을 생성하여 해당 변경 사항을 기록할 수 있습니다.
    • 버전 관리를 통해 여러 팀원이 작업할 때도 데이터베이스 스키마 일관성을 유지할 수 있습니다.
    • 예를 들어, prisma migrate dev --name init을 실행하면 초기 데이터베이스 스키마를 정의한 마이그레이션 파일을 생성합니다.
  3. Prisma Studio:
    • 데이터베이스 내 데이터를 시각적으로 조회하고 수정할 수 있는 웹 인터페이스입니다.
    • Prisma Studio는 개발자에게 현재 데이터 상태를 쉽게 파악할 수 있도록 해주며, 데이터베이스 관리 인터페이스로 활용할 수 있습니다.

Prisma의 핵심 기능

  • 타입 안전성(Type Safety):
    • Prisma는 TypeScript를 지원하며 데이터베이스 구조에 맞춰 자동으로 타입을 생성해줍니다.
    • 이러한 타입 안전성을 통해 런타임에서 발생할 수 있는 오류를 컴파일 단계에서 미리 방지할 수 있습니다.
  • 직관적인 데이터 모델링:
    • schema.prisma 파일에서 데이터베이스 모델을 정의합니다.
    • 모델링한 데이터베이스 구조는 Prisma Client를 통해 자동으로 사용 가능한 쿼리 API 형태로 제공됩니다.
  • 다양한 데이터베이스 지원:
    • PostgreSQL, MySQL, SQLite, MongoDB 등을 지원하며, 다양한 데이터베이스와 쉽게 연결하고 사용할 수 있습니다.

Prisma와 함께하는 워크플로우

  1. Prisma Schema 작성:
    • schema.prisma 파일에서 데이터베이스 모델을 정의합니다.
    • 예를 들어, User 테이블을 다음과 같이 정의할 수 있습니다.
model User {
  id    Int    @id @default(autoincrement())
  email String @unique
  name  String
}

2. 마이그레이션 파일 생성 및 적용:

  • npx prisma migrate dev --name <migration_name> 명령어로 데이터베이스에 마이그레이션 파일을 적용합니다.

3. Prisma Client 사용:

  • 모델에 맞춰 자동 생성된 Prisma Client API를 통해 CRUD 작업을 수행합니다.
  • 예를 들어, 사용자를 생성하려면 다음과 같이 쿼리를 작성할 수 있습니다.
 const newUser = await prisma.user.create({
  data: {
    email: "user@example.com",
    name: "John Doe",
  },
});

 

4. Prisma Studio로 데이터 확인:

  • npx prisma studio를 통해 웹 인터페이스로 데이터베이스에 접근하고 데이터를 확인할 수 있습니다.

Prisma의 장점

  • 개발 생산성 증가: 코드 작성 속도를 높여주고, 데이터베이스 쿼리 작업에 대한 직관적인 접근 방식을 제공합니다.
  • 자동 완성 지원: TypeScript와 호환되며 자동 완성을 통해 정확한 데이터베이스 필드를 쉽게 찾고 사용할 수 있습니다.
  • 유연성: Prisma의 데이터 모델링 시스템은 여러 데이터베이스 간의 일관성을 유지하면서도, 프로젝트에 맞춘 유연한 구조를 설계할 수 있습니다.

+ Recent posts