코딩/React

카카오 로그인 구현 React(리액트) Nextjs NextAuth kakao login

드리프트 2021. 10. 4. 11:27
728x170

 

 

 

안녕하세요?

 

지난 시간에는 NextJS와 MongoDB로 유저 로그인 세션 구현하기에 도전해 봤는데요.

 

최근에는 직접 유저 가입과 그 정보를 DB에 저장하는 거는 굉장히 위험한 일입니다.

 

그래서 각 대표 포털에서는 각자의 로그인 방식을 공유하고 있는데요.

 

우리 나라 사람이라면 다 있다고 해도 무방한 카카오 아이디, 네이버 아이디와 구글 아이디로 로그인하는 방법에 대해 알아 보겠습니다.

 

물론, 싱글 페이지 앱이 아닌 NextJS와 NextAuth를 이용해서 나중에 확장 가능하도록 만들 예정입니다.

 

 

1. NextJS template 설치

 

일단 콘솔에서 다음과 같이 입력하여 create-next-app을 이용하여 nextjs template을 만들도록 하겠습니다.

 

npx create-next-app kakao-naver-login-next-auth --ts

 

이제 카카오와 네이버 로그인에 필요한 NextAuth를 설치합시다.

 

https://next-auth.js.org/

 

NextAuth.js

Authentication for Next.js

next-auth.js.org

그리고 Prisma 라고 NodeJS에서 DB제어를 쉽게 해주는 패키지를 설치할 예정입니다.

 

npm install next-auth @prisma/client

그리고 devDependencies로 다음과 같이 추가로 설치합시다.

npm install @types/node @types/next-auth prisma -D

 

다 설치하고 나서의 package.json 파일의 내용입니다.

 

여기서 중요하게 볼게 next-auth 의 버전인데요.

 

next-auth는 버전이 3.29.0 입니다.

 

그런데, 지금 기준 (2021년 10월 3일) NextAuth는 Version 4.0 베타가 한창 개발중이고 모든 Documentation도 Version 4를 기준으로 한겁니다.

 

prisma 도 3.1.1 버전이고 prisma/client도 버전이 2.30.3 인걸 일단은 기억하고 계십시요.

 

자, 이제 NextJS 앱을 실행해 볼까요?

 

npm run dev

익숙한 NextJS 초기 화면이 나왔습니다.

 

자, 우리가 여기서 짚고 넘어가야 할게 있는데요.

 

UI 부분을 어떻게 할것인가입니다.

 

초기에는 로직부분만 신경쓰고 UI 부분은 나중에 로직이 완성되면 작성하는 방법이 있고요.

 

아니면 처음부터 UI부분과 같이 로직부분도 같이 작성하는 방법이 있습니다.

 

우리는 두번째 방법을 이용하도록 하겠습니다.

 

일단은 뭔가 작품이 멋져 보여야 겠죠.

 

그래서 최근에 가장 좋아하는 UI 라이브러이인 Chakra-UI를 설치하도록 하겠습니다.

 

npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4 @chakra-ui/icons react-icons@4.2.0

Chakra-ui icons과 react-icons 도 설치했습니다.

 

react-icons 는 4.2.0을 설치하십시요. 4.3.0은 에러가 납니다. 나중에 고쳐지겠지만요.

 

 

 

2. UI Layout 작성

 

일단 제가 가장 많이 쓰는 Layout과 Header, Footer 그리고 Menu 코드를 올리겠습니다.

 

일단 styles 폴더를 삭제하십시요. 안하셔도 됩니다.

 

일단 _app.tsx 파일을 Chakra-ui 가 적용되도록 바꿉시다.

 

/pages/_app.tsx

import React from "react";
import Head from "next/head";
import Layout from "../components/Layout";

import { Provider } from "next-auth/client";

import { ChakraProvider } from "@chakra-ui/react";
import type { AppProps } from "next/app";

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <>
      <Head>
        <title>Kakao Naver Login NextAuthJS Prisma App</title>
      </Head>
      <Provider session={pageProps.session}>
        <ChakraProvider resetCSS>
          <Layout>
            <Component {...pageProps} />
          </Layout>
        </ChakraProvider>
      </Provider>
    </>
  );
}
export default MyApp;

next-auth/client Provider 가 모든 컴포넌트를 감싸게 만들었습니다.

 

이거 나중에 모든 컴포넌트에서 session을 사용하기 위해서 꼭 해야 합니다.

 

그리고 ChakraProvider 가 모든 컴포넌트를 감싸게 만들었습니다.

 

그리고 Layout.tsx 파일을 만들어 보겠습니다.

 

/components/Layout.tsx

import React from "react";
import Header from "./Header";
import Footer from "./Footer";
import { Container } from "@chakra-ui/react";

type Props = {
  children: React.ReactNode;
};

export default function Layout(props: Props) {
  return (
    <Container maxW="container.xl" p={0}>
      <Header />
      {props.children}
      <Footer />
    </Container>
  );
}

 

Header와 Footer도 만들어 볼까요?

 

먼저 Footer입니다.

 

/components/Footer.tsx

import React from "react";
import {
  Flex,
  Stack,
  Text,
  VStack,
  Divider,
  ButtonGroup,
  IconButton,
} from "@chakra-ui/react";
import { FaGithub, FaLinkedin, FaTwitter } from "react-icons/fa";
import Link from "next/link";

interface MenuItemProps {
  m: React.ReactNode;
  children: string;
  to: string;
}
const MenuItem = ({ children, to = "/" }: MenuItemProps) => {
  return (
    <Text
      ms={{ base: 2, sm: 2, md: 2, xl: 2 }}
      mr={{ base: 2, sm: 2, md: 2, xl: 2 }}
      display="block"
    >
      <Link href={to}>{children}</Link>
    </Text>
  );
};

const Footer = () => {
  return (
    <VStack direction={["column", "column", "column", "column"]}>
      <Divider />
      <Flex
        px={{ base: "4", md: "8" }}
        py="4"
        as="footer"
        wrap="wrap"
        w="100%"
        mx="auto"
      >
        <Stack
          direction="row"
          spacing="4"
          align="center"
          justify="space-between"
          w="100%"
        >
          <MenuItem m={4} to="https://cpro95.tistory.com/50">
            카카오톡 웹 버전 만들기
          </MenuItem>
          <ButtonGroup variant="ghost" color="gray.600">
            <IconButton
              as="a"
              href="https://github.com/cpro95/kakao-cpro95"
              target="_blank"
              aria-label="GitHub"
              icon={<FaGithub fontSize="20px" />}
            />
            <IconButton
              as="a"
              href="https://twitter.com/cpro95"
              target="_blank"
              aria-label="Twitter"
              icon={<FaTwitter fontSize="20px" />}
            />
          </ButtonGroup>
        </Stack>
      </Flex>
      <MenuItem m={4} to="/">
        &copy; 2021 , made by cpro95@gmail.com
      </MenuItem>
    </VStack>
  );
};

export default Footer;

 

Header를 알아볼까요?

 

/components/Header.tsx

import {
  Box,
  Flex,
  Text,
  IconButton,
  Stack,
  Collapse,
  Icon,
  Link,
  Popover,
  PopoverTrigger,
  PopoverContent,
  useColorModeValue,
  useBreakpointValue,
  useDisclosure,
} from "@chakra-ui/react";
import {
  HamburgerIcon,
  CloseIcon,
  ChevronDownIcon,
  ChevronRightIcon,
} from "@chakra-ui/icons";

import { DarkModeSwitch } from "./DarkModeSwitch";
import { Children } from "react";

export default function WithSubnavigation() {
  const { isOpen, onToggle } = useDisclosure();
  const myBgColor = "#F9E000";
  return (
    <Box>
      <Flex
        bg={useColorModeValue("white", "gray.800")}
        color={useColorModeValue("gray.600", "white")}
        minH={"60px"}
        py={{ base: 4 }}
        px={{ base: 4 }}
        borderBottom={1}
        borderStyle={"solid"}
        borderColor={useColorModeValue("gray.200", "gray.900")}
        align={"center"}
        bgColor={myBgColor}
      >
        <Flex
          flex={{ base: 1, md: "auto" }}
          ml={{ base: -2 }}
          display={{ base: "flex", md: "none" }}
        >
          <IconButton
            onClick={onToggle}
            icon={
              isOpen ? (
                <CloseIcon color="gray.800" w={3} h={3} />
              ) : (
                <HamburgerIcon color="gray.800" w={5} h={5} />
              )
            }
            variant={"ghost"}
            aria-label={"Toggle Navigation"}
          />
        </Flex>
        <Flex flex={{ base: 1 }} justify={{ base: "center", md: "start" }}>
          <Text
            textAlign={useBreakpointValue({ base: "center", md: "left" })}
            fontFamily={"heading"}
            color="gray.800"
            fontSize={"2xl"}
            fontWeight={"bold"}
          >
            <Link style={{ textDecoration: "none" }} href="/">
              홈
            </Link>
          </Text>

          <Flex display={{ base: "none", md: "flex" }} ml={10}>
            <DesktopNav />
          </Flex>
        </Flex>

        <Stack
          flex={{ base: 1, md: 0 }}
          justify={"flex-end"}
          direction={"row"}
          spacing={6}
        >
          <DarkModeSwitch />
        </Stack>
      </Flex>

      <Collapse in={isOpen} animateOpacity>
        <MobileNav />
      </Collapse>
    </Box>
  );
}

const DesktopNav = () => {
  const linkColor = useColorModeValue("gray.800", "gray.800");
  const linkHoverColor = useColorModeValue("gray.800", "gray.800");
  const popoverContentBgColor = useColorModeValue("white", "gray.800");

  return (
    <Stack direction={"row"} spacing={4} align="center">
      {NAV_ITEMS.map((navItem) => (
        <Box key={navItem.label}>
          <Popover trigger={"hover"} placement={"bottom-start"}>
            <PopoverTrigger>
              <Link
                p={2}
                href={navItem.href ?? "#"}
                fontSize={"sm"}
                fontWeight={500}
                color={linkColor}
                _hover={{
                  textDecoration: "none",
                  color: linkHoverColor,
                }}
              >
                {navItem.label}
              </Link>
            </PopoverTrigger>

            {navItem.children && (
              <PopoverContent
                border={0}
                boxShadow={"xl"}
                bg={popoverContentBgColor}
                p={4}
                rounded={"xl"}
                minW={"sm"}
              >
                <Stack>
                  {navItem.children.map((child) => (
                    <DesktopSubNav key={child.label} {...child} />
                  ))}
                </Stack>
              </PopoverContent>
            )}
          </Popover>
        </Box>
      ))}
    </Stack>
  );
};

const DesktopSubNav = ({ label, href, subLabel }: NavProps) => {
  return (
    <Link
      href={href}
      role={"group"}
      display={"block"}
      p={2}
      rounded={"md"}
      _hover={{ bg: useColorModeValue("white", "gray.800") }}
    >
      <Stack direction={"row"} align={"center"}>
        <Box>
          <Text
            transition={"all .3s ease"}
            _groupHover={{ color: "blue.400" }}
            fontWeight={500}
          >
            {label}
          </Text>
          <Text fontSize={"sm"}>{subLabel}</Text>
        </Box>
        <Flex
          transition={"all .3s ease"}
          transform={"translateX(-10px)"}
          opacity={0}
          _groupHover={{ opacity: "100%", transform: "translateX(0)" }}
          justify={"flex-end"}
          align={"center"}
          flex={1}
        >
          <Icon color={"pink.400"} w={5} h={5} as={ChevronRightIcon} />
        </Flex>
      </Stack>
    </Link>
  );
};

const MobileNav = () => {
  return (
    <Stack
      bg={useColorModeValue("white", "gray.800")}
      p={4}
      display={{ md: "none" }}
    >
      {NAV_ITEMS.map((navItem) => (
        <MobileNavItem key={navItem.label} {...navItem} />
      ))}
    </Stack>
  );
};

const MobileNavItem = ({ label, children, href }: NavProps) => {
  const { isOpen, onToggle } = useDisclosure();

  return (
    <Stack spacing={4} onClick={children && onToggle}>
      <Flex
        py={2}
        as={Link}
        href={href ?? "#"}
        justify={"space-between"}
        align={"center"}
        _hover={{
          textDecoration: "none",
        }}
      >
        <Text
          fontWeight={600}
          color={useColorModeValue("gray.600", "gray.200")}
        >
          {label}
        </Text>
        {children && (
          <Icon
            as={ChevronDownIcon}
            transition={"all .25s ease-in-out"}
            transform={isOpen ? "rotate(180deg)" : ""}
            w={6}
            h={6}
          />
        )}
      </Flex>

      <Collapse in={isOpen} animateOpacity style={{ marginTop: "0!important" }}>
        <Stack
          mt={2}
          pl={4}
          borderLeft={1}
          borderStyle={"solid"}
          borderColor={useColorModeValue("gray.200", "gray.700")}
          align={"start"}
        >
          {children &&
            children.map((child: NavProps) => (
              <Link key={child.label} py={2} href={child.href}>
                {child.label}
              </Link>
            ))}
        </Stack>
      </Collapse>
    </Stack>
  );
};

interface ChildrenProps {
  label: string;
  href: string;
}

interface NavProps {
  label: string;
  href?: string;
  subLabel?: string | null;
  children?: Array<ChildrenProps>;
}

const NAV_ITEMS = [
  {
    label: "홈",
    href: "/",
  },
  {
    label: "설명",
    href: "/intro",
  },
  {
    label: "API 살펴보기",
    children: [
      {
        label: "카카오 로그인",
        href: "/wiki/kakao",
      },
      {
        label: "네이버 로그인",
        href: "/wiki/naver",
      },
    ],
  },
];

 

그리고 DarkModeSwitch.tsx파일입니다.

 

/components/DarkModeSwitch.tsx

import { useColorMode, IconButton } from "@chakra-ui/react";
import { MoonIcon, SunIcon } from "@chakra-ui/icons";

export const DarkModeSwitch = () => {
  const { colorMode, toggleColorMode } = useColorMode();
  return (
    <IconButton
      aria-label="Toggle Dark Switch"
      icon={
        colorMode === "dark" ? (
          <SunIcon color="black" />
        ) : (
          <MoonIcon color="black" />
        )
      }
      onClick={toggleColorMode}
      ms={{ base: 2, sm: 2, md: 2, xl: 2 }}
      mr={{ base: 2, sm: 2, md: 2, xl: 2 }}
      display="block"
      w={10}
      h={10}
    />
  );
};

DarkModeSwitch는 Chakra-UI가 기본 기능인 다크모드와 라이트모드 토글 기능입니다.

 

이제 index.tsx파일을 고쳐서 실행화면을 볼까요?

 

/pages/index.tsx

import React from "react";
import type { NextPage } from "next";
import Head from "next/head";

const Home: NextPage = () => {
  return (
    <div>
      <Head>
        <title>Kakao Naver Login NextAuthJS Prisma App</title>
        <meta name="description" content="Kakao Naver NextAuth Prisma App" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <h1>Kakao Naver Login NextAuthJS Prisma App</h1>
    </div>
  );
};

export default Home;

 

 

실행화면을 볼까요?

 

라이트 모드일 경우

 

다크 모드일 경우
모바일용 화면

 

 

카카오 색깔인 노란색을 Header.tsx 파일에서 myBgColor로 지정했습니다.

 

노란색이 싫으시다면 myBgColor를 원하는 컬러를 지정하시면 됩니다.

 

const myBgColor = "#F9E000";

 

 

3. 카카오 디벨로퍼 설정

 

카카오 로그인을 할려면 카카오에 디벨로퍼(Developer)로 등록해야 합니다.

 

https://developers.kakao.com/

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

로그인 하시면 내 애플리케이션을 누르십시요.

 

저는 저번에 kakaoweb.netlify.app을 만들기 위해 기존에 만들어 놓은 애플리케이션이 있네요.

 

위에 + 버튼을 눌러 애플리케이션 추가하기를 누릅시다.

 

화면이 뜨면 앱 이름에 적당한 이름을 적어 줍시다.

 

저는 NextAuth-cpro59라고 했습니다.

 

그리고 사업자명도 적어야 하는데 저는 사업자가 아니기 때문에 앱 이름과 같은 이름으로 적었습니다.

 

이제 다음과 같이 전체 애플리케이션이 뜹니다.

 

이제 방금 만든 NextAuth-cpro95를 클릭합니다.

 

앱 키가 나왔네요.

 

중요한 정보니까 일단 REST API 키를 일단 어디다 저장해 놓으세요.

 

밑에 플랫폼 설정하기 파란색 링크를 클릭하십시요.

 

우리는 Web 앱을 만들 예정이라 마지막에 있는 Web 플랫폼 등록 버튼을 누릅니다.

 

여기는 주소를 넣는 창인데요.

 

일단 개발 주소인 http://localhost:3000을 넣읍시다.

 

나중에 배포할 때는 배포할 주소를 넣으시면 됩니다.

 

Web 플랫폼 등록하셨으면 맨 밑에 또 다른 링크가 나오죠?

 

가장 중요한 링크입니다.

 

카카오 로그인 사용시 Redirect URI를 등록해야 합니다.

 

이에 뭐냐면요. 우리의 NextJS앱에서 카카오 로그인을 위해 카카오 로그인 URL로 이동합니다.

 

그리고 카카오 로그인이 성공되면 다시 우리의 NextJS앱으로 오게끔 해야 되는데요.

 

그 때 필요한 Callback 링크가 필요합니다.

 

우리가 앞으로 이용할 NextAuthJS는 특정 Callback Redirect URI를 지정해 주기 때문에 이부분도 쉽게 해결가능합니다.

 

그럼 파란색 등록하러 가기 버튼을 클릭합시다.

위와 같이 뜨는데요.

 

일단 카카오 로그인을 활성화 해야 하니까 상태 OFF를 ON으로 바꿉시다.

 

 

그리고 밑에 Redirect URI를 누르고 아래와 같이 입력합시다.

 

 

최종적인 모습입니다.

 

 

이제 카카오 로그인을 사용할 수 있게 카카오 디벨로퍼 설정이 끝났습니다.

 

그리고 위에서 어디다 저장해 놓은 REST API 앱키는 잘 있겠죠?

 

이 걸 NextAuth 에서 이용할 예정입니다.

 

 

 

4. Prisma setting

 

카카오 로그인이나 네이버 로그인을 추가하고 각각의 로그인으로 로그인하면 카카오나 네이버 모두 토큰을 전달해 주는데요.

 

JWT 토큰입니다.

 

그러면 이 토큰과 세션을 저장하기 위한 DB가 필요한데요.

 

이 때 필요한게 Prisma 입니다.

 

일단 다음과 같이 Prisma 을 실행합시다.

 

npx prisma init

일단 실행이 완료되면 두개의 파일이 생성됩니다.

 

먼저, .env 파일이니다.

 

NextJS에서 중요한 환경변수를 저장하는 .env 파일입니다.

 

DATABASE_URL 이 있네요.

 

Prisma 가 사용할 DB주소를 기본적으로 저장한겁니다.

 

Prisma 는 기본적으로 PostgreSQL과 MySQL, MSSQL, Sqlite를 지원합니다.

 

MongoDB는 현재 베타 버전이고요.

 

우리는 Sqlite 를 사용할 예정입니다.

 

Sqlite 로컬 파일을 만들어서 거기에 DB를 저장할 예정입니다.

 

 

두번째 생성된 파일은 /prisma/schema.prisma 파일입니다.

 

 

prisma 의 스키마를 정하는 파일인데요.

 

일단 datasource db 가 postgresql로 되어 있네요.

 

일단 이것 부터 바꾸겠습니다.

 

 

.env 파일입니다.

 

/.env

# Environment variables declared in this file are automatically made available to Prisma.

# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server (Preview) and MongoDB (Preview).

# See the documentation for all the connection string options: https://pris.ly/d/connection-strings


DATABASE_URL="file:./dev.db"

file 확장자로 로컬에 dev.db라는 이름으로 sqlite DB를 이용할 예정입니다.

 

그리고 schema.prisma 파일입니다.

 

/prisma/schema.prisma

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

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

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

model User {
  id            Int       @id @default(autoincrement())
  name          String?
  email         String?   @unique
  emailVerified DateTime? @map(name: "email_verified")
  image         String?
  createdAt     DateTime  @default(now()) @map(name: "created_at")
  updatedAt     DateTime  @updatedAt @map(name: "updated_at")

  @@map(name: "users")
}

model Account {
  id                 Int       @id @default(autoincrement())
  compoundId         String    @unique @map(name: "compound_id")
  userId             Int       @map(name: "user_id")
  providerType       String    @map(name: "provider_type")
  providerId         String    @map(name: "provider_id")
  providerAccountId  String    @map(name: "provider_account_id")
  refreshToken       String?   @map(name: "refresh_token")
  accessToken        String?   @map(name: "access_token")
  accessTokenExpires DateTime? @map(name: "access_token_expires")
  createdAt          DateTime  @default(now()) @map(name: "created_at")
  updatedAt          DateTime  @default(now()) @map(name: "updated_at")


  @@index([providerAccountId], name: "providerAccountId")
  @@index([providerId], name: "providerId")
  @@index([userId], name: "userId")
  @@map(name: "accounts")
}

model Session {
  id           Int      @id @default(autoincrement())
  userId       Int      @map(name: "user_id")
  expires      DateTime
  sessionToken String   @unique @map(name: "session_token")
  accessToken  String   @unique @map(name: "access_token")
  createdAt    DateTime @default(now()) @map(name: "created_at")
  updatedAt    DateTime @default(now()) @map(name: "updated_at")

  @@map(name: "sessions")
}

 

뭔가 복잡합니다.

 

model 은 SQL Table 같은건데요.

 

Prisma는 위와 같이 model 방식으로 PostgreSQL, MySQL, MSSQL, SQLite 모두 적용될 수 있게 해주는 패키지입니다.

 

그래서 Prisma 만 배우면 위의 모든 DB 적용은 아주 쉽게 되는 거죠.

 

일단 카카오 로그인이나 네이버 로그인을 할려면 User,  Account, Session 정보를 저장할 DB가 필요합니다.

 

그래서 Prisma로 해당 되는 정보를 model로 만들었습니다.

 

이 정보는 NextAuth 홈페이지에 Prisma Adapter 부분에 나와 있는데 NextAuth가 지금 한참 Ver 4.0 베타버전입니다.

 

그래서 Ver 3에서 제시하는 model 부분을 이용해도 카카오나 네이버 로그인이 작동되지 않았습니다.

 

왜냐하면 카카오나 네이버 로직이 옛날 로직이라서 그런거 같네요.

 

이거 때문에 하루 종일 삽질했는데요.

 

겨우 찾았습니다.

 

위와 같이 User, Accout, Session 모델을 설정하십시요.

 

일단 Prisma 모델을 설정했으면 이걸 NextJS에서 사용할 수 있게 변환해야 합니다.

 

다음과 같이 하시면 됩니다

npx prisma db push

이렇게 하면 prisma 폴더에 우리가 지정한 이름인 dev.db 파일이 생성됩니다.

 

이 dev.db 파일은 sqlite 파일입니다.

 

그럼 제대로 prisma model 이 적용됐는지 볼까요?

 

prisma 는 studio 를 지원합니다. 아주 좋은 기능인데요.

 

다음과 같이 실행합시다.

 

npx prisma studio

 

위와 같이 실행하면 localhost:5555 주소로 창이 뜨면서 다음과 같이 나타납니다.

 

아까 Schema 파일에서 지정했던 Model 이 나와있네요.

 

각각 클릭해 봅시다.

 

User 모델을 클릭하면 나타나는 화면입니다.

 

Add record 로 직접 User를 삽입할 수 있고 삭제도 할 수 있습니다.

 

아주 막강한 기능인데요.

 

그래서 Prisma 라이브러리가 요즘 뜨고 있나 봅니다.

 

NextJS앱에서 Prisma 라이브러리를 쓸려면 @prisma/client가 필요합니다.

 

우리는 맨 처음부터 이 패키지를 설치했기 때문에 당연히 있을 겁니다.

 

그리고 우리의 Prisma Schema 파일을 NextJS앱에서 사용할 수 있게 prisma client를 만들어야 합니다.

 

다음과 같이 입력합시다.

 

npx prisma generate

Prisma 클라이언트가 생성됐고 어떻게 사용하는지 코드도 보여주네요.

 

우리가 앞으로 Prisma 를 이용해서 DB 작업을 할때 매번 Prisma 클라이언트를 호출해야 하는데 Prisma 클라이언트가 중복 될 수 있기 때문에 유니버셜하게 한개만 로드하는 lib 파일을 만들어 보겠습니다.

 

/lib/prisma.ts

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

let prisma: PrismaClient;

if (process.env.NODE_ENV === 'production') {
    prisma = new PrismaClient();
} else {
    if (!global.prisma) {
        global.prisma = new PrismaClient();
    }
    prisma = global.prisma;
}

export default prisma;

 

 

5. Next-Auth 설정

 

카카오 로그인을 하기 위해서는 NextAuth 설정파일을 작성해야 합니다.

 

/pages 폴더 밑에 api 폴더가 있고 그 밑에 auth 폴더를 만듭시다.

 

그리고 그 밑에 [...nextauth].ts 파일을 만들고 다음과 같이 넣어줍시다.

 

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

import { NextApiHandler } from 'next';
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import Adapters from 'next-auth/adapters';
import prisma from '../../../lib/prisma';

const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;

const options = {
    providers: [
        Providers.Kakao({
            clientId: process.env.KAKAO_CLIENT_ID,
            clientSecret: process.env.KAKAO_CLIENT_SECRET
        }),
    ],
    adapter: Adapters.Prisma.Adapter({ prisma }),
};

위 코드는 NextAuth의 설정파일입니다.

 

NextAuth는 Providers 를 여러개 지원하는데요. 구글, 트위터, 깃헙, 카카오, 네이버 등 거의 모든 로그인을 지원합니다.

 

우리는 카카오를 쓰기 위해 위와 같이 했습니다.

 

그럼 .env 파일을 수정해 볼까요?

 

# Environment variables declared in this file are automatically made available to Prisma.

# See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server (Preview) and MongoDB (Preview).

# See the documentation for all the connection string options: https://pris.ly/d/connection-strings


DATABASE_URL="file:./dev.db"

KAKAO_CLIENT_ID=fc268a9cbf715--------------
KAKAO_CLIENT_SECRET=yoursecret


NEXTAUTH_URL=http://localhost:3000/api/auth

우리가 카카오 디벨로퍼에서 얻은 앱키 중에 REST API 앱키가 바로 CLIENT_ID입니다

 

CLIENT_ID에 넣어 주시면 되고

 

CLIENT_SECRET는 아무 문장이나 적으십시요.

 

처음에는 CLIENT_SECRET가 카카오 앱키가 들어가는 줄 알았는데 이거 때문에 꽤 고생했습니다.

 

카카오 디벨로퍼나 NextAuth 설명서에는 안 나와 있는 겁니다.

 

그리고 중요한게 바로 NEXTAUTH_URL입니다.

 

Callback Redirect URI 와 관련된겁니다.

 

localhost:3000/api/auth 부분이 가장 기본적인 콜백 리다이렉트 URI인 거죠.

 

자, 그럼, index.tsx 파일에 로그인 버튼을 추가해 봅시다.

 

 

/pages/index.tsx

import React from "react";
import type { NextPage } from "next";
import Head from "next/head";
import { signIn, signOut, useSession } from "next-auth/client";
import { Flex, Button, Heading, VStack } from "@chakra-ui/react";

const Home: NextPage = () => {
  const [session, loading] = useSession();

  if (typeof window !== "undefined" && loading) return null;
  
  return (
    <div>
      <Head>
        <title>Kakao Naver Login NextAuthJS Prisma App</title>
        <meta name="description" content="Kakao Naver NextAuth Prisma App" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <Flex justify="center">
        <VStack mb={6}>
          <Heading mb={6}>Kakao Naver Login NextAuthJS Prisma App</Heading>

          {!session && (
            <Button colorScheme="facebook" onClick={signIn}>
              sign in
            </Button>
          )}

          {session && (
            <Button colorScheme="twitter" onClick={signOut}>
              sign out
            </Button>
          )}
        </VStack>
      </Flex>
    </div>
  );
};

export default Home;

SignI 버튼과 SignOut 버튼을 추가했는데요.

 

Chakra-UI를 이용했습니다.

 

그리고 여기서 중요한 Hook 이 있는데요.

 

바로 next-auth/client 에서 제공하는 useSession 훅입니다.

 

useSession 훅은 현재 로그인 되어 있는 Session 을 이용할 수 있습니다.

 

loading 변수는 세션을 구할 때까지 시간이 걸릴때를 나타내는 변수이고요.

 

그럼 실행화면을 볼까요?

 

 

 

next-auth/client는 signIn 과 signOut 같은 좋은 함수도 제공해 줍니다.

 

그래서 위 사진처럼 버튼을 클릭ㅎ면 SignIn 화면으로 이동할 수 있는거죠.

 

한번 클릭해 볼까요?

 

위 사진 처럼 뜹니다.

 

카카오로 Sign in 할 수 있는 링크가 뜨는데요.

 

이 링크는 NextAuth가 기본적으로 만든 링크 화면입니다.

 

나중에 이걸 본인 입맛에 맞게 바꿀 수 있는데 그건 다음에 알아 보겠습니다.

 

위 버튼을 클릭하면 다음과 같이 뜹니다.

 

카카오 서비스 연결을 위한 동의화면이 뜨네요.

 

동의하고 계속하기 버튼을 눌러봅시다.

 

익숙한 카카오 로그인 화면이 떴습니다.

 

로그인 해볼까요?

 

로그인 성공하면 아래와 같이 뜹니다.

 

 

즉, session 이 성립됐다는 뜻입니다.

 

prisma studio 화면으로 가볼까요?

 

 

 

Session, Account 가 잘 생성되었습니다.

 

그런데 User 부분에서 name, email, image 등이 모두 null이네요.

 

session을 console.log 해보아도 밑에 사진과 마찬가지입니다.

session.user 객체에 보통 카카오에서 전달하는 name, email, image 가 저장되는데 이부분이 안되는거 같습니다.

 

원인을 파악해 볼까요?

 

카카오 디벨로퍼 화면에서 카카오 로그인 밑에 동의항목이란 부분이 있습니다.

 

여기를 잘 보시면 닉네임: 사용 안함 상태고요.

 

프로필 사진: 사용 안함 상태입니다.

 

그리고 카카오계정(이메일): 사용 안함 상태입니다.

 

이 세개를 사용함으로 변경합시다.

 

 

 

 

 

위와같이 동의 목전은 테스트라고 적어주시고 저장하시면 됩니다.

 

 

참고로 카카오계정(이메일)은 선택 동의 밖에 안됩니다.

 

그럼 우리 앱에서 다시 카카오로 로그인 하면 바로 아래 처럼 동의하기 화면이 나옵니다.

 

 

이제 다시 signOut 하고 signIn 해볼까요?

 

이때 prisma studio 에서 Session, Account, User 부분의 데이터를 다 지우고 하십시요.

 

 

세션을 초기화하는 겁니다.

 

그럼 다시 로그인 해볼까요?

 

 

 

위 그림처럼 name부분과 email부분 그리고 image부분의 정보가 카카오에서 잘 넘어왔습니다.

 

index.tsx 부분에 다음과 같이 코드를 추가해 볼까요?

 

{session && (
            <Image w={64} h={64} src={session.user.image} alt="image" />
          )}

 

Chara-UI 의 Image 컴포넌트로 이미지를 보여주는데요.

 

src 부분이 session.user.image 입니다.

 

 

session.user.image 부분이 아주 잘 보이네요.

 

이제 카카오 로그인 부분은 완성되었습니다.

 

다음 시간에는 네이버 로그인에 대해 알아 보겠습니다.

 

 

그리드형