๋ณธ ๊ธ์ ํจ์คํธ์บ ํผ์ค โ Next.js ์ค๋ฌด ๊ฐ์ ์ค Part 8. Next.js 13์ผ๋ก ์๋ฐ ์์ฝ ํ๋ซํผ ๋ง๋ค๊ธฐ๋ฅผ ์๊ฐํ๋ฉฐ ํ์ตํ ๋ด์ฉ์ ์ ๋ฆฌํ ๊ฒ์ ๋๋ค. ๐๐ป
NextAuth.js๋ OAuth ๊ธฐ๋ฐ์ ์ธ์ฆ ํ๋ก์ฐ๋ฅผ ์ฝ๊ฒ ๊ตฌ์ฑํ ์ ์๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ค. ๊ฐ๋จํ ์ค์ ๋ง์ผ๋ก๋ ์์ ๋ก๊ทธ์ธ, ์ธ์ ๊ด๋ฆฌ, ์ฌ์ฉ์ ์ ๋ณด ์ ์ฅ๊น์ง ๊ตฌํํ ์ ์์ผ๋ฉฐ, Next.js์์ ํธํ์ฑ๋ ๋ฐ์ด๋๋ค.
์ฃผ์ ํน์ง
- ๊ฐํธํ ์ค์ : ์ต์ํ์ ์ค์ ๋ง์ผ๋ก ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๋น ๋ฅด๊ฒ ๊ตฌ์ฑ ๊ฐ๋ฅ
- ๋ค์ํ ์ธ์ฆ ์ ๊ณต์: Google, Naver, Kakao ๋ฑ ์์ ๋ก๊ทธ์ธ ์ง์
- ๋ณด์ ๊ธฐ๋ฅ ๋ด์ฅ: ์ธ์ , JWT, CSRF ๋ฑ ์๋ ์ฒ๋ฆฌ
- DB ์ฐ๋ ๊ฐ๋ฅ: ์ฌ์ฉ์ ๊ณ์ ยท์ธ์ ์ ๋ณด ์ ์ฅ ๋ฐ ํ์ฉ ๊ฐ๋ฅ
- Next.js ์์ ํธํ: CSR๊ณผ SSR ๋ชจ๋์์ ์์ ์ ์ผ๋ก ์๋
์ฝ๋ฐฑ(callback) ํจ์ ์ ๊ณต
NextAuth๋ ์ธ์ฆ ํ๋ฆ์ ์ธ๋ถ์ ์ผ๋ก ์ ์ดํ ์ ์๋๋ก ์ฝ๋ฐฑ ํจ์(callbacks)๋ฅผ ์ ๊ณตํ๋ค. ์ธ์ฆ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ, ๋ฆฌ๋๋ ์
, ์ธ์
ยทJWT ๋ฐ์ดํฐ ๊ตฌ์ฑ ๋ฑ ๋ค์ํ ์์ ์ ๊ฐ์
ํ ์ ์๋ค.
callbacks: {
signIn, // ๋ก๊ทธ์ธ ์ฑ๊ณต ์ฌ๋ถ ๊ฒฐ์
redirect, // ๋ก๊ทธ์ธ ํ ๋ฆฌ๋๋ ์
๊ฒฝ๋ก ์ค์
session, // ์ธ์
์ ์ ๋ฌํ ๋ฐ์ดํฐ ์์
jwt // JWT ํ ํฐ ๋ฐ๊ธ/๊ฐฑ์ ์ ๋ฐ์ดํฐ ์กฐ์
}
JWT vs ์ธ์
๊ธฐ๋ฐ ์ธ์ฆ ๋ฐฉ์
NextAuth๋ ์ธ์
๊ธฐ๋ฐ๊ณผ JWT ๊ธฐ๋ฐ ์ธ์ฆ ๋ฐฉ์์ ๋ชจ๋ ์ง์ํ๋ค.
๐ JWT vs ์ธ์ ๊ธฐ๋ฐ ์ธ์ฆ ๋ฐฉ์ ์ฐจ์ด
- JWT (JSON Web Token)
- ํด๋ผ์ด์ธํธ์ ์ ์ฅ๋๋ ๋ฌด์ํ(stateless) ํ ํฐ
- ์์ฒญ๋ง๋ค ํ ํฐ์ ํค๋์ ํฌํจ์์ผ ์ฌ์ฉ์ ์๋ณ
- ์๋ฒ ์ธ์ ์ด ํ์ ์์ด ํ์ฅ์ฑ์ด ์ข์
- ๋จ ํ ํฐ์ด ํ์ทจ๋ ๊ฒฝ์ฐ ์ํํ๋ฏ๋ก ๋ง๋ฃ ์๊ฐ๊ณผ ์ ์ฅ ์์น์ ์ฃผ์ํด์ผ ํจ
- ์ธ์ (Session)
- ์ฌ์ฉ์ ์ํ๋ฅผ ์๋ฒ์์ ์ง์ ๊ด๋ฆฌ
- ํด๋ผ์ด์ธํธ์๋ ์ธ์ ID๋ง ์ ์ฅ
- ๋น๊ต์ ์์ ํ์ง๋ง ์๋ฒ ์์์ด ์๋ชจ๋จ
- ํด๋ฌ์คํฐ ํ๊ฒฝ์์๋ Redis ๋ฑ์ผ๋ก ์ธ์ ๊ณต์ ํ์
1. ํจํค์ง ์ค์น
yarn add next-auth
2. ํ๊ฒฝ๋ณ์ ์ถ๊ฐ
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=๋ณต์กํ๊ณ _์์ธก๋ถ๊ฐ๋ฅํ_๋๋ค๋ฌธ์์ด
3. ์ธ์ฆ ํธ๋ค๋ฌ(Route) ํ์ผ ์์ฑ
App Router ๊ธฐ์ค์ผ๋ก app/api/auth/[...nextauth]/route.ts
๊ฒฝ๋ก์ ํ์ผ์ ์์ฑํ๋ค.
4. ์ธ์ฆ ๊ณต๊ธ์ ๋ฐ ์ต์
์ค์
์์ฑํ route
ํ์ผ ๋ด๋ถ์ Google, Naver ๋ฑ ์ํ๋ provider
๋ฅผ ๋ฑ๋กํ๊ณ ์ฝ๋ฐฑ ๋ฐ ์ธ์
์ค์ ์ ์ถ๊ฐํ๋ค.
5. route.ts ํ์ผ์์ ์ธ์ฆ ๊ณต๊ธ์ ๋ฑ๋ก
์์ฑํ route ํ์ผ ๋ด๋ถ์ Google, Naver ๋ฑ ์ํ๋ provider๋ฅผ ๋ฑ๋กํ๊ณ ์ฝ๋ฐฑ ๋ฐ ์ธ์
์ค์ ์ ์ถ๊ฐํ๋ค. clientId
, clientSecret
์ ํ๊ฒฝ๋ณ์์์ ๋ถ๋ฌ์จ๋ค.
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
})
6. SessionProvider๋ก ์ธ์ฆ ์ ๋ณด ์ ์ญ ์ ์ฉ
ํด๋ผ์ด์ธํธ ํ๊ฒฝ์์ ๋ก๊ทธ์ธ ์ํ๋ฅผ ์ ์ญ์์ ์ฌ์ฉํ ์ ์๋๋ก SessionProvider
๋ฅผ ์ค์ ํ๋ค. app/providers.tsx
๋๋ layout.tsx
๋ฑ ์ต์์ ์ปดํฌ๋ํธ์ ๋ฑ๋กํ๋ค
import { SessionProvider } from 'next-auth/react'
๐ก ๋ก๊ทธ์ธ ์ํ ํ์ธ๊ณผ ์ธ์ ๊ด๋ฆฌ
๋ก๊ทธ์ธ ์ฌ๋ถ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ํ์ธํ๋ ค๋ฉดuseSession()
ํ ์ ์ฌ์ฉํ๋ฉด ๋๋ค.
status
๋ก ์ธ์ฆ ์ํ๋ฅผ ํ๋จํ๊ณsession
๊ฐ์ฒด๋ก ์ฌ์ฉ์ ์ ๋ณด์ ์ ๊ทผํ ์ ์๋ค.const { data: session, status } = useSession() if (status === 'authenticated') { return <p>Welcome, {session.user.email}</p> } return <Link href="/api/auth/signin">Sign in</Link>
๐ก ์ธ์ฆ์ด ํ์ํ ํ์ด์ง ๋ณดํธํ๊ธฐ (Middleware ์ค์ )
NextAuth๋middleware
๋ฅผ ํตํด ํน์ ๊ฒฝ๋ก์ ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์๋ง ์ ๊ทผํ ์ ์๋๋ก ์ ์ดํ ์ ์๋ค. (ex. ๋ง์ดํ์ด์ง, ๊ฐ์ธ์ ๋ณด ์์ ๋ฑ)
- .env์
NEXTAUTH_SECRET
์ ๋ฐ๋์ ์ค์ /src/middleware.ts
ํ์ผ ์์ฑ ํ ์๋์ ๊ฐ์ด ์์ฑ// middleware.ts export { default } from 'next-auth/middleware' export const config = { matcher: ['/users/mypage', '/users/info', '/users/edit'], }
NextAuth๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์ธ์ฆ ์ํ๋ง ๊ด๋ฆฌํ์ง๋ง, PrismaAdapter๋ฅผ ํจ๊ป ์ฌ์ฉํ๋ฉด ์ฌ์ฉ์ ๊ณ์ , ์์
๊ณ์ , ์ธ์
์ ๋ณด๋ฅผ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋์ผ๋ก ์ ์ฅํ ์ ์๋ค. ์ด๋ ํ์ํ ๋ชจ๋ธ์ User
, Account
, Session
, VerificationToken
์ด๋ฉฐ, schema.prisma
ํ์ผ์ ์ ์ํ ๋ค prisma migrate
๋ก ๋ฐ์ํ๋ฉด ๋๋ค.
1. Prisma Adapter ์ค์น
yarn add @auth/prisma-adapter
2. Adapter ์ ์ฉ
next-auth
์ค์ ํ์ผ(route.ts
)์์ ์๋์ฒ๋ผ adapter๋ฅผ ๋ฑ๋กํ๋ฉด ๋๋ค.
import { PrismaAdapter } from '@auth/prisma-adapter'
import prisma from '@/db'
export const authOptions = {
adapter: PrismaAdapter(prisma),
providers: [...],
...
}
3. Prisma ์คํค๋ง ์ ์ ๋ฐ ๋ง์ด๊ทธ๋ ์ด์
PrismaAdapter
๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด next-auth
๊ฐ ์๊ตฌํ๋ 4๊ฐ์ง ๋ชจ๋ธ์ schema.prisma
์ ์ ์ํด์ผ ํ๋ค. (์ ์ฒด ์คํค๋ง ๊ตฌ์กฐ๋ ๊ณต์ ๋ฌธ์์์ ํ์ธํ ์ ์๋ค.)
User
/ Account
/ Session
/ VerificationToken
๊ตฌ๊ธ ๊ฐ๋ฐ์ ์ฝ์์์ ํด๋ผ์ด์ธํธ๋ฅผ ์์ฑํ๊ณ , ๋ฐ๊ธ๋ฐ์ Client ID
, Client Secret
์ .env
์ ๋ฑ๋กํ๋ค. NextAuth ์ค์ ํ์ผ(route.ts
)์์ GoogleProvider
๋ฅผ ์ถ๊ฐํ๋ฉด ์ฐ๋์ด ์๋ฃ๋๋ค.
1. Google Cloud ํด๋ผ์ด์ธํธ ์์ฑ
ํ๋ก์ ํธ ์์ฑ ํ ์ฌ์ฉ์ ์ธ์ฆ ์ ๋ณด > OAuth 2.0 ํด๋ผ์ด์ธํธ ID ์์ฑ ํ ์์ฑ๋ Client ID์ Client Secret์ .env์ ์ถ๊ฐํ๋ค.GOOGLE_CLIENT_ID=... GOOGLE_CLIENT_SECRET=...
์น์ธ๋ ๋ฆฌ๋๋ ์ URI์ ์๋ ๊ฒฝ๋ก ์ถ๊ฐํ๋ค.
http://localhost:3000/api/auth/callback/google
2. next-auth์ GoogleProvider ๋ฑ๋ก
app/api/auth/[...nextauth]/route.ts
ํ์ผ์ ์๋ ์ฝ๋๋ฅผ ์ถ๊ฐํ๋ค.export const authOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID || '', clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', }), ], ... }
adapter: PrismaAdapter(prisma)
๋ฅผ ์ฌ์ฉํ๋ฉด ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ ๋ณด๊ฐ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์๋์ผ๋ก ์ ์ฅ๋๋ค. User
, Account
, Session
ํ
์ด๋ธ์ ํ์ํ ์ ๋ณด๊ฐ ๊ฐ๊ฐ ๋ค์ด๊ฐ๊ธฐ ๋๋ฌธ์ ๋ณ๋๋ก ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ด๋ฆฌํ๋ ๋ก์ง์ ์์ฑํ์ง ์์๋ ๋๋ค.GoogleProvider
๋ฅผ ์ถ๊ฐํด์ผ ํ๋ค. ๊ตฌ๊ธ ๊ฐ๋ฐ์ ์ฝ์์์ ๋ฐ๊ธ๋ฐ์ clientId
์ clientSecret
๊ฐ์ ์ค์ ํ๋ฉด ๋๋ค.NextAuth
๋ ๊ธฐ๋ณธ ๋ก๊ทธ์ธ ํ๋ฉด๋ ์ ๊ณตํ๋ค. ๋ก๊ทธ์ธ ํ์ด์ง๋ฅผ ๊ตฌ์ฑํ /users/signin
ํ์ด์ง๋ฅผ ๋ง๋ค์ด ์ฐ๊ฒฐํ๋ค.NextAuth
๋ ๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ด๋ฉ์ผ๊ณผ ์ด๋ฆ ์ ๋๋ง ์ธ์
์ ๋ด์์ค๋ค. DB์ ์ ์ฅ๋ ์ฌ์ฉ์ ID๋ ํด๋ผ์ด์ธํธ์์ ์ฌ์ฉํ ํ์๊ฐ ์์ด callback
์ ์ค์ ํ์ฌ ์ธ์
์ user.id
๊ฐ์ ์ถ๊ฐ๋ก ํฌํจ์์ผฐ๋ค. ๋ก๊ทธ์ธ ํ์ด์ง์์๋ signIn()
ํจ์๋ฅผ ์ฌ์ฉํด ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ๋ฒํผ์ ํด๋ฆญํ๋ฉด OAuth ์ธ์ฆ์ด ์์๋๋๋ก ๊ตฌ์ฑํ๋ค. ๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ฉด, ์ง์ ํ callbackUrl
(์ฌ๊ธฐ์๋ ๋ฉ์ธ ํ์ด์ง /)๋ก ์๋ ์ด๋ํ๋๋ก ์ฒ๋ฆฌํ๋ค.
๋ค์ด๋ฒ์ ์นด์นด์ค ์์
๋ก๊ทธ์ธ๋ ํจ๊ป ๊ตฌํํ๋ค. ๊ฐ ํ๋ซํผ์ ๊ฐ๋ฐ์ ์ฌ์ดํธ(NAVER Developers, Kakao Developers)์ ์ฑ์ ๋ฑ๋กํ ๋ค, ๋ฐ๊ธ๋ฐ์ Client ID
, Secret
, Redirect URI
์ค์ ํ provider
์ต์
์ ์์ฑํด์ฃผ๋ฉด ๊ตฌ๊ธ ๋ก๊ทธ์ธ๊ณผ ๋์ผํ ๋ฐฉ์์ผ๋ก ์ฐ๋ํ ์ ์๋ค.
๋ค๋ง ํ๋ซํผ๋ง๋ค ๋ฐํ๋๋ ๋ฐ์ดํฐ ๊ตฌ์กฐ๊ฐ ์กฐ๊ธ์ฉ ๋ฌ๋ผ prisma์ ์คํค๋ง๋ ๊ทธ์ ๋ง๊ฒ ์กฐ์ ์ด ํ์ํ๋ค. ์๋ฅผ ๋ค์ด ์นด์นด์ค์ ๊ฒฝ์ฐ refresh_token_expires_in
๊ณผ ๊ฐ์ ํ๋๊ฐ ๋ฐ๋ก ์ ๋ฌ๋๊ธฐ ๋๋ฌธ์ ํด๋น ๊ฐ์ ์ ์ฅํ๊ณ ์ ํ๋ค๋ฉด Account
๋ชจ๋ธ์ ํด๋น ํ๋๋ฅผ ์ถ๊ฐํด์ค์ผ ํ๋ค. ์ด์ฒ๋ผ ๊ฐ ์์
๋ก๊ทธ์ธ ์ ๊ณต์์ ๋ฐํ ๊ฐ์ ๋ฐ๋ผ ์คํค๋ง๋ฅผ ์กฐ์ ํด์ผ ํ๋ฏ๋ก, ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํด ํ๋ ์ ์๋ฅผ ๋ช
ํํ ํด๋๋ ๊ฒ์ด ์ค์ํ๋ค.
์ค์ ๋ก Supabase์ ์์ฑ๋ ์ฌ์ฉ์ ๋ฐ ๋ก๊ทธ์ธ ๊ด๋ จ ํ
์ด๋ธ์ ์๋์ ๊ฐ๋ค. ์์
๋ก๊ทธ์ธ(Google, Naver, Kakao ๋ฑ)์ ์งํํ ํ ์ฌ์ฉ์ ์ ๋ณด๊ฐ User
, Account
, Session
ํ
์ด๋ธ์ ์๋์ผ๋ก ์ ์ฅ๋๋ ๊ฒ์ ํ์ธํ ์ ์์๊ณ , Prisma Adapter
๋ฅผ ํตํด NextAuth
์์ ์ฐ๋๋ ๋ฌธ์ ์์ด ์ ์๋ํ๋ค. ๐คฉ
๋ก๊ทธ์ธํ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์๋ฒ์์ ์์ ํ๊ฒ ์กฐํํ ์ ์๋๋ก API๋ฅผ ๊ตฌํํ๋ค.
getServerSession()
์ ํตํด ํ์ฌ ์ธ์
์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ณ , ์ธ์ฆ๋ ์ฌ์ฉ์๋ผ๋ฉด Prisma๋ฅผ ์ฌ์ฉํด ์ฌ์ฉ์ ์ ๋ณด์ ์ฐ๊ฒฐ๋ ์์
๊ณ์ (Account) ์ ๋ณด๊น์ง ํจ๊ป ์กฐํํ๋ค.
์๋ต์ NextResponse.json()
์ผ๋ก ๋ฐํํ๋ฉฐ ์ธ์ฆ๋์ง ์์ ๊ฒฝ์ฐ์๋ 401 Unauthorized
์ํ ์ฝ๋์ ํจ๊ป ์๋ฌ ๋ฉ์์ง๋ฅผ ์ ๋ฌํ๋ค.
ํด๋ผ์ด์ธํธ ์ธก์์๋ useSession()
ํ
์ ํ์ฉํด ๋ก๊ทธ์ธ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๊ณ , ๋ง์ดํ์ด์ง ์๋จ์ ์ฌ์ฉ์ ์ด๋ฆ๊ณผ ์ด๋ฉ์ผ์ ํ์ํ๋ค.
๋ง์ดํ์ด์ง์๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ์์ธํ ๋ณด์ฌ์ฃผ๋ ๊ฐ์ธ์ ๋ณด ํ์ด์ง๋ฅผ ๋ฐ๋ก ๊ตฌ์ฑํ๋ค.
์ด ํ์ด์ง๋ React Query๋ฅผ ํ์ฉํด ์๋ฒ์์ ์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ๋ถ๋ฌ์ค๋ฉฐ enabled
์ต์
์ ์ค์ ํด ๋ก๊ทธ์ธ๋ ๊ฒฝ์ฐ์๋ง API๊ฐ ํธ์ถ๋๋๋ก ํ๋ค.
const { data: user } = useQuery('user', fetchUser, {
enabled: status === 'authenticated',
})
๋ถ๋ฌ์จ ์ ๋ณด๋ ์ด๋ฆ, ์ด๋ฉ์ผ, ํ๋กํ ์ด๋ฏธ์ง, ์ฃผ์, ์ ํ๋ฒํธ, ๋ก๊ทธ์ธํ SNS ๋ฑ์ผ๋ก ๊ตฌ์ฑ๋๋ฉฐ ํ์ด์ง ํ๋จ์๋ ๋ก๊ทธ์์ ๊ธฐ๋ฅ๋ ํจ๊ป ์ ๊ณตํ๋ค. signOut()
ํจ์ ํธ์ถ ์ ์ธ์
์ด ์ข
๋ฃ๋๊ณ , ๋ฉ์ธ ํ์ด์ง(/
)๋ก ์๋ ์ด๋๋๋ค.
- tailwindcss Form Layouts
๊ธฐ๋ณธ์ ์ธ ํผ UI๋ Tailwind CSS์์ ์ ๊ณตํ๋ Form Layout ์์ ๋ฅผ ์ฐธ๊ณ ํด ๊ตฌํํ๋ค. ๊ทธ๋ฆฌ๋ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ฑ๋ ์ ๋ ฅ ํ๋์ ์น์ ์ ๋ชฉ ์คํ์ผ๋ง ๋ฑ์ ์ ํธ๋ฆฌํฐ ํด๋์ค๋ฅผ ๊ทธ๋๋ก ํ์ฉํด ๋น ๋ฅด๊ฒ ๊ตฌ์ถํ ์ ์์๋ค.
์กฐํ๋ ์ฌ์ฉ์ ์ ๋ณด๋ useEffect
๋ฅผ ํตํด useState๋ก ๊ตฌ์ฑ๋ ํผ ์ํ(name, email, phone, address)์ ์ด๊ธฐ๊ฐ์ผ๋ก ํ ๋นํ๋ค.
API ํธ์ถ ์์๋ refetchOnMount: false
์ต์
์ ์ฌ์ฉํด ๋ฆฌ๋ ๋๋ง ์ ์ค๋ณต ํธ์ถ์ ๋ฐฉ์งํ๋ค.
์ฌ์ฉ์๊ฐ ํ๋๋ฅผ ์์ ํ๋ฉด onChange
์ด๋ฒคํธ์์ ๊ฐ ํ๋์ ์ํ๋ฅผ ์
๋ฐ์ดํธํ๊ณ , '์์ ํ๊ธฐ' ๋ฒํผ ํด๋ฆญ ์ axios.put()
์ผ๋ก /api/users
์ ์์ ๋ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ค.
์์ฒญ์ด ์ฑ๊ณตํ๋ฉด react-hot-toast
๋ก ์ฑ๊ณต ๋ฉ์์ง๋ฅผ ๋์ฐ๊ณ , useRouter().replace()
๋ฅผ ํตํด ๋ค์ ๊ฐ์ธ์ ๋ณด ํ์ด์ง(/users/info
)๋ก ์ด๋์ํจ๋ค.