이전 글에서 app 디렉토리에 소셜로그인 구현하는 것 까지 기록해두었는데, 그 코드를 기반으로 진행해보자.
[Next.js 13]Next.js 13.2버젼에 Next-auth 구현하기(+ 네이버 소셜 로그인)
물론 next-auth가 자동으로 제공해주지만, 서비스 완성도 향상을 위해서는 필수적인 부분이 아닐까 싶다.
사실 여기부터는 next.js 13.2 이후 버전인거는 크게 관련 없다고 생각한다.
어디서 csr을 사용해야 하는지 생각해야 하는 정도인 것 같다.
src/app/api/auth/[...nextauth]/route.ts
```tsx
import NextAuth from "next-auth";
import NaverProvider from "next-auth/providers/naver";
...
const handler = NextAuth({
providers: [
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID || "",
clientSecret: process.env.NAVER_CLIENT_SECRET || "",
}),
... //구글이랑 카카오도 써보는 중
],
});
export { handler as GET, handler as POST };
```
src/app/api/auth/[...nextauth]/route.ts
import NextAuth, { NextAuthOptions } from "next-auth";
import NaverProvider from "next-auth/providers/naver";
...
export const authOptions: NextAuthOptions = {
providers: [
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID || "",
clientSecret: process.env.NAVER_CLIENT_SECRET || "",
}),
... //구글이랑 카카오도 써보는 중
],
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
그 이유는 authOptions가 커스텀 로그인 페이지 및 여러 곳에서 필요하기 때문이다.
route.ts
의 authOptions에 객체 타입을 받는 pages를 추가하자....
pages: {
signIn: '/auth/signin',
signOut: '/auth/signout',
error: '/auth/error', // Error code passed in query string as ?error=
verifyRequest: '/auth/verify-request', // (used for check email message)
newUser: '/auth/new-user' // New users will be directed here on first sign in
}
...
로그인, 로그아웃, 에러(로그인 관련 에러), 인증 요청, 최초 가입자 페이지같은 유용한 정보들이 나와있고 error에는 에러 메세지를 ?error=
뒤에 오는 query string으로 전달해준다고 한다.'/auth/signin'
으로 하고,app/auth/signin
에 페이지를 만들자.src/app/api/auth/[...nextauth]/route.ts
import NextAuth, { NextAuthOptions } from "next-auth";
import NaverProvider from "next-auth/providers/naver";
...
export const authOptions: NextAuthOptions = {
providers: [
NaverProvider({
clientId: process.env.NAVER_CLIENT_ID || "",
clientSecret: process.env.NAVER_CLIENT_SECRET || "",
}),
...
],
pages: {
signIn: "/auth/signin", // 내가 원하는 커스텀 sign-in 페이지의 url
},
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
export default function SigninPage() {
return <div>Sign in Page</div>
}
/api/auth/signin
으로 변경된걸 확인할 수 있다. 이제 이 페이지 안에 providers를 가지고 와서 하나씩 버튼으로 나열해주자.app/api/auth/signin/page.tsx
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType
} from 'next'
import { getProviders, signIn } from 'next-auth/react'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '../[...nextauth]/route'
export default function SignInPage({
providers
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<>
{Object.values(providers).map(provider => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
</>
)
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context.req, context.res, authOptions)
if (session) {
return { redirect: { destination: '/' } }
}
const providers = await getProviders()
return {
props: { providers: providers ?? [] }
}
}
💡 ****Failed to compile****
./app/api/auth/signin/page.tsx
ReactServerComponentsError:
"getServerSideProps" is not supported in app/.
//session 정보 가져오기
const session = await getServerSession(context.req, context.res, authOptions)
//session이 있으면 Home으로 보내기
if (session) {
return { redirect: { destination: '/' } }
}
//authOption에서 providers받아오기
const providers = await getProviders()
return {
props: { providers: providers ?? [] }
}
app/api/auth/signin/page.tsx
```tsx
import { getProviders, signIn } from 'next-auth/react'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '../[...nextauth]/route'
export default async function SignInPage() {
const session = await getServerSession(authOptions)
if (session) {
return { redirect: { destination: '/' } }
}
const providers = await getProviders()
return (
<>
{Object.values(providers).map(provider => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
</>
)
}
```
💡 **Unhandled Runtime Error**
Error: Event handlers cannot be passed to Client Component props.
<button onClick={function} children=...>
^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
<button/>
태그를 없애고 그냥 provider 이름만 나열하게 해보자```tsx
<div className='m-4'>
{Object.values(providers).map(provider => (
<div key={provider.name}>Sign in with {provider.name}</div>
))}
</div>
```
SocialSigninButton
컴포넌트를 만들어서 넣어주자. app/components/SocialSigninButton.tsx
'use client';
import { signIn } from 'next-auth/react';
type IProps = {
providers
}
export default function SocialSigninButton({ providers }: IProps) {
return (
<div>
{Object.values(providers).map(provider => (
<div key={provider.name}>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
</div>
);
}
Record<LiteralUnion<BuiltInProviderType, string>, ClientSafeProvider>
우선, 앞에 있는 리터럴 유니온은 다음 코드 처럼 타입을 선택할 수 있게 해주는 타입스크립트의 기능이다.type BuiltInProviderType = 'A' | 'B' | 'C';
type MyType = LiteralUnion<BuiltInProviderType, 'D'>;
string과 묶여있고, signin함수 내에 provider는 string으로 집어넣으니 여기서는 string이라고 하자. 뒤의 ClientSafeProvider
는 next-auth에서 만들어준 타입이니 그냥 그대로 집어넣자. 이걸 Record에 담아주자. app/components/SocialSigninButton.tsx
'use client'
import { ClientSafeProvider, signIn } from 'next-auth/react'
type IProps = {
providers: Record<string, ClientSafeProvider>
}
export default function SocialSigninButton({ providers }: IProps) {
return (
<div>
{Object.values(providers).map(provider => (
<div key={provider.name} className='m-4 bg-slate-200'>
<button onClick={() => signIn(provider.id)}>
Sign in with {provider.name}
</button>
</div>
))}
</div>
)
}
여기서부터는 찾아보다가 인도인 형님의 도움을 받았다.
아 이거네.. callbackUrl을 useSearchParams에서 꺼내오고 있다.
useSearchParams는 next.js의 라우터(네비게이션) 패키지에서 제공하는 훅으로, URL에서 쿼리 파라미터를 꺼내오고 조작하게 해준다.
signin 페이지에서 url 창을 보면 아래와 같이 써있는걸 볼 수 있다.
http://localhost:3000/api/auth/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2F
useSearchParams()가 여기서 callbackUrl을 받아서 signin메서드에 전달해주면 된다.
app/components/SocialSigninButton.tsx
'use client'
import { useSearchParams } from 'next/navigation'
import { ClientSafeProvider, signIn } from 'next-auth/react'
type IProps = {
providers: Record<string, ClientSafeProvider>
}
export default function SocialSigninButton({ providers }: IProps) {
const searchParams = useSearchParams()
const callbackUrl = searchParams.get('callbackUrl')
console.log(searchParams)
console.log(callbackUrl)
console.log(callbackUrl)
console.log(callbackUrl)
console.log(callbackUrl)
return (
<div>
{Object.values(providers).map(provider => (
<div key={provider.name} className='m-4 bg-slate-200'>
<button onClick={() => signIn(provider.id, { callbackUrl })}>
Sign in with {provider.name}
</button>
</div>
))}
</div>
)
}
이제 아주 평화롭고 깔끔하게 home으로 이동했다!