Next.js์์ ์์ ํ ๋ก๊ทธ์ธ ๊ตฌํ ์ฌ์ (without NextAuth)
๊ตฌํ ๋ชฉํ
์ ํฌ๋ ์ค์ ๋ก ์๋น์ค๋ฅผ ์ด์ํ๋ ๊ฒ์ด ๋ชฉํ์๊ธฐ ๋๋ฌธ์ ์ ์ ์ธ์ฆ ๊ด๋ จ ๋ก์ง๋ค์์๋ ๋ณด์์ด ๋งค์ฐ ์ค์ํ๊ณ , ๊ทธ์ ๋ฐ๋ผ ์๊ตฌ์ฌํญ์ ๋ค์๊ณผ ๊ฐ์์ต๋๋ค
์๊ตฌ์ฌํญ
- ์ธ์ฆ ๊ด๋ จ ๋ก์ง์ ํด๋ผ์ด์ธํธ์์ ์ฒ๋ฆฌํ์ง ์์ ๊ฒ
- ํด๋ผ์ด์ธํธ์์ ์คํฌ๋ฆฝํธ๋ก ์ ๊ทผํ ์ ์๋ ๊ณณ์ ๊ฐ์ธ์ ๋ณด๋ฅผ ๋จ๊ธฐ์ง ์์ ๊ฒ ex) ๋ก์ปฌ์คํ ๋ฆฌ์ง, ์ธ์ ์คํ ๋ฆฌ์ง, ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ, ์ฟ ํค ๋ฑ
- ๋ก๊ทธ์ธ ํ ์๋ก๊ณ ์นจ์ ํด๋ ์ผ์ ์๊ฐ๋์์ ๋ก๊ทธ์ธ ์ํ๊ฐ ํ๋ฆฌ์ง ์์ ๊ฒ
- ๊ตฌ๊ธ์์ ์ง์ํ๋ OAuth ํ๋กํ ์ฝ์ ์ฌ์ฉํ ๊ฒ
๋ํ Next.js ๋ฅผ ์ฐจ์ฉํ๊ธฐ ๋๋ฌธ์ ์๋ฒ ์ปดํฌ๋ํธ์์๋ ๋์์ด ๊ฐ๋ฅํ์ด์ผ ํ์ต๋๋ค
์ ๋ธ๋ผ์ฐ์ ์ ๊ฐ์ธ์ ๋ณด๋ฅผ ์ ์ฅํ๋ฉด ์๋ ๊น?
Redux ๋๋ Zustand ๊ฐ์ ์ ์ญ์ํ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ ์ ์ญ ์ํ์ ์ ์ง๋ฅผ ์ํด ๋ฏธ๋ค์จ์ด์ ํํ๋ก persist ๋ฅผ ์ง์ํ๋๋ฐ์, ๋ฆฌ์กํธ ๊ฐ๋ฐ์๋ผ๋ฉด ๋ก๊ทธ์ธ์ ๊ตฌํํ ๋ persist ๋ฅผ ๊ณ ๋ คํด๋ณธ ์ ์ด ์์ ๊ฒ ๊ฐ์ต๋๋ค
๊ผญ persist ๊ฐ ์๋๋๋ผ๋ ๋ก์ปฌ์คํ ๋ฆฌ์ง ๊ฐ์ ๊ณณ์ ์ ์ ์ ๋ณด๋ฅผ ์ ์ฅํด๋๊ณ ์ฐ๋ฉด ํธ๋ฆฌํ๋ฐ ์ ์ ์ฅํ์ง ๋ง๋ผ๊ณ ํ๋ ๊ฑธ๊น์?
์กฐ๊ธ ์ง๋ถํ ๋๋ต์ด์ง๋ง ์ญ์๋ XSS ๋๋ CSRF ๋ฅ์ ๊ณต๊ฒฉ์ ๋๋นํ๊ธฐ ์ํจ์ ๋๋ค
XSS (๊ต์ฐจ ์ฌ์ดํธ ์คํฌ๋ฆฝํ ) | CSRF (๊ต์ฐจ ์ฌ์ดํธ ์์ฒญ ์์กฐ) | |
๊ฐ์ | ์ ์ฑ ์คํฌ๋ฆฝํธ๊ฐ ํด๋ผ์ด์ธํธ์์ ์คํ๋จ | ๊ณต๊ฒฉ์๊ฐ ์ ๋ขฐํ ์ ์๋ ์ฌ์ฉ์๋ฅผ ์ฌ์นญํ๊ณ ์น ์ฌ์ดํธ์ ์์น ์๋ ์์ฒญ์ ๋ณด๋ |
๊ณต๊ฒฉ ๋์ | ํด๋ผ์ด์ธํธ | ์๋ฒ |
๋ชฉ์ | ์ธ์ ํ์ทจ, ์น์ฌ์ดํธ ๋ณ์กฐ | ๊ถํ ๋์ฉ |
๋ฆฌ์กํธ์์๋ ๊ธฐ๋ณธ์ ์ผ๋ก innerHTML์ ๋ง์๋์๊ธฐ ๋๋ฌธ์ ์์ ๊ฐ์ด ๋จ์ํ ๊ณต๊ฒฉ ํจํด์ ๋จนํ์ง ์๊ฒ ์ง๋ง, ๋ง์ฝ ๋ณด์์ด ๋ซ๋ ธ์ ๊ฒฝ์ฐ ์คํฌ๋ฆฝํธ๋ก ์ ๊ทผํ ์ ์๋ ๋ชจ๋ ์ ๋ณด๋ ํ์ทจ๋ ์ ์๋ค๊ณ ์๊ฐํด์ผํฉ๋๋ค
๋ง์ฝ ์คํฌ๋ฆฝํธ๋ก ์ ๊ทผํ ์ ์๋ ๊ณณ์ ์ ์ ์ ๊ฐ์ธ์ ๋ณด๊ฐ ํฌํจ๋์ด ์๋ค๋ฉด ๋ณด์ ์ทจ์ฝ์ ์ด ๋ฐ์ํ์ ๋ ๊ณต๊ฒฉ์์๊ฒ ๋ฌด๋ฐฉ๋น๋ก ์ ์ ๋ค์ ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ด์ด์ค ์ ๋ฐ์ ์๊ฒ ์ฃ ?
ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๋ก์จ ํ ์ ์๋ ์ต์ ์ ๋ธ๋ผ์ฐ์ ์์ ์ทจ์ฝ์ ์ด ๋ฐ์ํ ์ ์๋ ์ฝ๋๋ฅผ ์ต์ํํ๊ณ , ์ ์ ์ ๊ด๋ จ๋ ๋ฏผ๊ฐํ ์ ๋ณด๋ฅผ ๋ค๋ฃฐ ๋๋ ์ต๋ํ ์๋ฒ์ ์์ํ๋ ๊ฒ์ ๋๋ค
์๋ฒ๋ ๋น๊ต์ ํด๋ผ์ด์ธํธ ๋ณด๋ค ๋ณด์ ์ํ์ด ์ ๊ธฐ ๋๋ฌธ์ด์ฃ
ํด๋ผ์ด์ธํธ์์ ๊ฐ์ฅ ๋ณด์์์ค์ด ๋์ ์ ์ฅ๋ฐฉ์์ ์ธ๋ฉ๋ชจ๋ฆฌ(๋ณ์)์ ๊ฐ์ ์ ์ฅํ๋ ๊ฒ์
๋๋ค
ํ์ง๋ง ๋จ๋
์ผ๋ก ์ฌ์ฉํ๋ฉด ์๋ก๊ณ ์นจํ ๋ ๋ง๋ค ํ๋ฐ๋๊ธฐ ๋๋ฌธ์ ์ ์ ๊ฒฝํ์ ํฐ ์ฐจ์ง์ด ์๊ธฐ์ฃ
๊ทธ๋ ๋ค๊ณ ์คํฌ๋ฆฝํธ๋ก ์ ๊ทผํ ์ ์๋ ๋ก์ปฌ์คํ ๋ฆฌ์ง, ์ธ์ ์คํ ๋ฆฌ์ง, ์ฟ ํค ๋ฑ์ ์ ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ธฐ์๋ XSS์ CSRF ๋ฅ์ ๊ณต๊ฒฉ์ ์ทจ์ฝํ ์ ์์ต๋๋ค
๊ทธ๋ผ ์ด๋ป๊ฒ ๊ตฌํํด์ผ ํ ๊น์?
๊ทธ๋์ ์ด๋ป๊ฒ ๊ตฌํํ๋?
๊ฒฐ๋ก ์ ์ผ๋ก ์ ํฌ๋ api ๋ผ์ฐํธ์ HttpOnly ์ SameSite ๋ฐ secure ์ต์ ์ ์ ์ฉํ ์ฟ ํค ๋ฐฉ์์ ์ฑํํ์๊ณ , ์๋ฒ ์ปดํฌ๋ํธ์ฉ ํ ์ ๋ง๋ค์ด ์๋ฒ ์ปดํฌ๋ํธ์์๋ถํฐ ์ ์ ์ ๋ณด๋ฅผ ๋ฐ์์์ ์ํ๋ฅผ ๊ด๋ฆฌํ์์ต๋๋ค
๋ํ rewrite ๋ผ๋ Next.js ์ ํ๋ก์ ๊ธฐ๋ฅ์ ํ์ฉํ์ฌ ํด๋ผ์ด์ธํธ์ ๋ชจ๋ ์์ฒญ์ ์ธ์ ์์ด๋ ์ฟ ํค๊ฐ ๋ด๊ธธ ์ ์๋๋ก ์ค๊ณํ์์ต๋๋ค
์ฟ ํค๋ ์ํํ๊ฑฐ ์๋์๋?
๋ธ๋ผ์ฐ์ ์์ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋๋๋ ์ฟ ํค๋ ์ฌ์ค XSS, CSRF ๊ณต๊ฒฉ์ ๋ชจ๋ ์ทจ์ฝํ์ง๋ง
ํน์ ์ต์ ์ ํตํด ์ทจ์ฝ์ ๋ค์ ์ต๋ํ ๋ณด์ํ ์ ์์ต๋๋ค
์ ์ฉํ ์ต์ ๋ค
- HttpOnly : ์ ์ฉ ์ ์๋ฐ์คํฌ๋ฆฝํธ ์ฝ๋ ์์์ ์ ๊ทผ์ด ๋ถ๊ฐ๋ฅํด์ง๋ฉฐ, HTTP ์์ฒญ์๋ง ํฌํจ๋ฉ๋๋ค
- SameSite : none , strict , lax ๋ฑ์ ์ต์ ์ด ์์ต๋๋ค
- None : ํน๋ณํ ์ค์ ์ ํ์ง ์๋ ์ต์ ์ ๋๋ค
- Strict : ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์๋ ํญ์ ์ ์ก๋์ง ์์ต๋๋ค
- Lax : ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ ์ค ์์ ํ ์์ฒญ์๋ง ์ ์ก๋ฉ๋๋ค
- secure : HTTPS๊ฐ ์๋ ํต์ ์์๋ ์ฟ ํค๋ฅผ ์ ์กํ์ง ์์ต๋๋ค
* SameSite ์ต์ ์ Strict ๋ก ์ค์ ํ๊ฒ ๋๋ฉด ๋ฆฌ๋ค์ด๋ ํธ ์์ฒญ์์๋ ์ฟ ํค๊ฐ ์ค๋ฆฌ์ง ์์ ํ์ Lax ์ต์ ์ผ๋ก ์ค์ ํ์์ต๋๋ค
์ฟ ํค๋ฅผ ์ฒ์์ ์ด๋ป๊ฒ ์์ฑํ ๊น?
OAuth ๋ฐฉ์์ ๋ก๊ทธ์ธ์ ๊ตฌํํ ๋๋ ๋ณดํต ํน์ ํ callbackURL ์ ์ค์ ํ๊ณ ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ์ ๊ฒฝ์ฐ code ๋ฅผ ํฌํจํ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ์ ํจ๊ป callbackURL ๋ก ์์ฒญ์ ๋ณด๋ด์ค๋๋ค
Next.js ๋ฅผ ์ฌ์ฉํ๋ค๋ฉด ์๋์ ๊ฐ์ด ์ง์ ํ callbackURL ์ api ๋ผ์ฐํธ๋ฅผ ์์ฑํ์ฌ ์๋ฒ ๋จ์์ ๋ก๊ทธ์ธ ๋ก์ง์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค
import { cookies } from 'next/headers'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { url } = request
const queryParams = new URL(url).searchParams
const code = queryParams.get('code')
const response = await fetch(`${process.env.๋ฐฑ์๋_์๋ฒ_์ฃผ์}/๋ก๊ทธ์ธ_API_๊ฒฝ๋ก`, {
method: 'POST',
body: JSON.stringify({ provider: 'google', code }),
})
if (!response.ok) {
throw new Error('๋ก๊ทธ์ธ ์คํจ')
}
return NextResponse.redirect(process.env.ํ๋ก ํธ_์๋ฒ_์ฃผ์)
}
/api/oauth/callback/url/route.ts ์์ ์ฝ๋
https://nextjs.org/docs/app/building-your-application/routing/route-handlers
์ ๋ก์ง์ผ๋ก ๋ชจ๋ ์ฒ๋ฆฌ๋๋ฉด ์ข๊ฒ ์ง๋ง ๋ฐฑ์๋์์๋ Set-Cookie ํค๋๋ฅผ ํตํด ์ธ์ ์์ด๋๋ฅผ ๋๊ฒจ์ฃผ์์ต๋๋ค
์๋๋ผ๋ฉด ๋ธ๋ผ์ฐ์ ์ ๋์ฐฉํ์๊ฒ ์ง๋ง ์ด๋ป๊ฒ ๋ณด๋ฉด ์ ํฌ๊ฐ ์ค๊ฐ์์ ์์ฒญ์ ํ๋ฒ ๊ฐ๋ก์ฑ๋ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ์ฒ๋ฆฌ ๋ก์ง์ด ํ์ํฉ๋๋ค
const responseCookies = response.headers.get('Set-Cookie')
const parsedCookies = responseCookies
.split(';')
.map((s) => s.trim().split('='))
.reduce((obj: { [key: string]: string }, [key, value]) => {
return Object.assign(obj, { [key]: value ?? true })
}, {})
cookies().set(SESSION_ID, parsedCookies[SESSION_ID], {
path: '/',
httpOnly: true,
sameSite: 'strict',
secure: true,
})
์๋ต๋ฐ์ ์ฟ ํค ํค๋ ํ์ฑ ํ ์๋ก์ด ์ฟ ํค ์ค์ ํ๋ ๋ก์ง
https://nextjs.org/docs/app/api-reference/functions/cookies
๋ง์ง๋ง์ผ๋ก api ๋ผ์ฐํธ์์ ์๋ฌ๊ฐ ํฐ์ง๋ฉด ๋ฐ๋ก 500๋ฒ๋ ์๋ฌ๊ฐ ํด๋ผ์ด์ธํธ์ ์ ๋ฌ๋๊ธฐ ๋๋ฌธ์ ์ ์ ํ ์๋ฌ์ฒ๋ฆฌ ๋ก์ง๊ณผ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ํ์ฉํ ์ธํฐ๋ ์ ์ฒ๋ฆฌ๊น์ง ํด์ฃผ๋ฉด ์์ฑ์ ๋๋ค
try {
// ๋ก๊ทธ์ธ ๋ก์ง
return NextResponse.redirect(`${process.env.ํ๋ก ํธ_์๋ฒ_์ฃผ์}/?๋ก๊ทธ์ธ-์ฑ๊ณต`)
} catch {
cookies().delete(SESSION_ID) // ์๋ฌ ๋ฐ์ ์ ๋จ์์๋ ์ฟ ํค ์ญ์ ์ฒ๋ฆฌ
return NextResponse.redirect(`${process.env.ํ๋ก ํธ_์๋ฒ_์ฃผ์}/?๋ก๊ทธ์ธ-์คํจ`)
}
์๋ฒ ์ปดํฌ๋ํธ์์๋ ์ด๋ป๊ฒ ์ ๊ทผํ ๊น?
Next.js ์์๋ ์๋ฒ ์ธก์์ ๋์ํ๋ fetch ํจ์๋ฅผ ํ์ฅํ์ฌ ์ ๊ณตํฉ๋๋ค
์์ฒด์ ์ผ๋ก ์ง์ํ๋ cookies API๋ฅผ ํตํด ์์ฒญ ํค๋์ ์ฟ ํค๋ฅผ ์ง์ ๋ด์์ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค
'use server'
import { cookies } from 'next/headers'
export default async function getUserInfo() {
const response = await fetch(`${process.env.๋ฐฑ์๋_์๋ฒ_์ฃผ์}/์ธ์ฆ์ด_ํ์ํ_API_๊ฒฝ๋ก`, {
headers: {
'Content-Type': 'application/json',
Cookie: `${SESSION_ID}=${cookies().get(SESSION_ID)?.value}`,
},
credentials: 'include',
cache: 'no-store',
next: { revalidate: 0 },
})
if (!response.ok) {
throw new Error('์๋ฌ ๋ฐ์')
}
const { data } = await res.json()
return { data, response }
}
cookies API๋ ์๋ฒ ์ปดํฌ๋ํธ์์๋ ๋๊ฐ์ด ๋์ํฉ๋๋ค
์๋๋ ์ด๋ฏธ ๋ก๊ทธ์ธ ํ ์ ์ ๋ฅผ ๋ฉ์ธํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธํ๋ ์ฝ๋๋ฅผ ํฌํจํ ๋ก๊ทธ์ธ ํ์ด์ง ์์์ ๋๋ค
import { cookies } from 'next/headers'
export default function SignInPage() {
const isLogin = cookies().has(SESSION_ID)
if (isLogin) {
return redirect(`${process.env.ํ๋ก ํธ_์๋ฒ_์ฃผ์}/?์ด๋ฏธ-๋ก๊ทธ์ธ-๋์ด์์`)
}
return <SignIn />
}
์ํ ๊ด๋ฆฌ๋ ์ด๋ป๊ฒ ํ๋?
ํ์ฌ๊น์ง์ ๋ก์ง์ผ๋ก๋ ๋ก๊ทธ์ธํ ์งํ๋ ์ ๋์ํ์ง๋ง ์๋ก๊ณ ์นจํ๋ฉด ๋ก๊ทธ์ธ์ด ํ๋ฆฌ๊ฒ๋ฉ๋๋ค
์ ํฌ๋ Next.js ์ layout.tsx ๋ฅผ ํ์ฉํ์ฌ ๋ฃจํธ ๊ฒฝ๋ก์ ์๋ฒ์ฉ ํ ์ ์ฌ์ฉํ์ฌ ์ ์ ์ ๋ณด๋ฅผ ๋ฐ์์จ ํ, props ๋ฅผ ํตํด context api ์ ๋๊ฒจ์ฃผ์ด ์ํ ๊ด๋ฆฌ๋ฅผ ๊ตฌํํ์์ต๋๋ค
'use server'
import type { PropsWithChildren } from 'react'
export default async function RootLayout({ children }: PropsWithChildren) {
const authProps = await useSession()
return <AuthProvider {...authProps}>{children}</AuthProvider>
}
/app/layout.tsx ์์ ์ฝ๋
'use server'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'
export default async function useSession(): Promise<DefaultAuthState> {
const cookieStore = cookies()
const sessionId = cookieStore.get(SESSION_ID)?.value
const isLogin = !!sessionId
if (!isLogin) {
return { isLogin: false }
}
try {
// ์ ์ ์ ๋ณด ์กฐํ ๋ก์ง
return { isLogin: true }
} catch {
return redirect(`${process.env.ํ๋ก ํธ_์๋ฒ_์ฃผ์}/?๋ก๊ทธ์ธ-์คํจ`)
}
}
useSession ์์ ์ฝ๋
๋ฃจํธ ๊ฒฝ๋ก์ layout.tsx ๋ ์๋ฒ ์ปดํฌ๋ํธ๊ธฐ ๋๋ฌธ์ ์๋ก๊ณ ์นจ์ ํ ๋ ๋ง๋ค ์๋ฒ์์ ์ ์ ์ ๋ณด๋ฅผ ๊ฐฑ์ ํ์ฌ context api ์ ์ธ๋ฉ๋ชจ๋ฆฌ๋ก ์ ์ฅ๋์ด ์์ ํ๊ฒ ๋ก๊ทธ์ธ์ ์ ์งํ ์ ์๊ฒ ๋์์ต๋๋ค
'use client'
import { PropsWithChildren, createContext, useContext } from 'react'
const AuthContext = createContext<DefaultAuthState>({
isLogin: false,
})
export default function useAuth() {
return useContext(AuthContext)
}
export default function AuthProvider({
children,
...authProps
}: PropsWithChildren<DefaultAuthState>) {
return (
<AuthContext.Provider value={authProps}>{children}</AuthContext.Provider>
)
}
AuthProvider ์์ ์ฝ๋
Protected Route ๊ตฌํ
Protected Route ์ ํธ์ ๋ณดํต HOC ํจํด์ผ๋ก ๋ง์ด ๊ตฌํํ๊ฒ ๋ฉ๋๋ค
ํ์ง๋ง ์ ํฌ๋ ์๋ฒ ์ปดํฌ๋ํธ์ธ page.tsx ์์ ์ฌ์ฉํด์ผ ํ์ง๋ง, context api ๋ฅผ ํ์ฉํ useAuth ๋ ์๋ฒ ์ปดํฌ๋ํธ์์๋ ์คํ๋ ์ ์์์ต๋๋ค
๊ทธ๋์ ์ ํฌ๋ ์กฐ๊ธ์ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก children props ๋ฅผ ํตํด withAuth ์ปดํฌ๋ํธ๋ฅผ ๊ตฌํํ์์ต๋๋ค
์ต๊ทผ jotai ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์์๋ ์ ์ญ ์ํ๊ด๋ฆฌ๋ฅผ SSR ํ๊ฒฝ์์๋ ์ฌ์ฉํ ์ ์๋ค๋ ๊ฒ์ผ๋ก ๋ณด์ ์ถํ์๋ HOC ํจํด์ผ๋ก ๋ฆฌํฉํ ๋ง ํด๋ณผ ์๋ ์๊ฒ ์ต๋๋ค
'use client'
import type { PropsWithChildren } from 'react'
import { useAuth } from '../model'
import NeedLogin './NeedLogin'
export default function WithAuth({ children }: PropsWithChildren) {
const { isLogin } = useAuth()
return isLogin ? children : <NeedLogin />
}
WithAuth.tsx ์์ ์ฝ๋
'use server'
export default function MyPage() {
return (
<WithAuth>
<MyPageContent />
</WithAuth>
)
}
WithAuth.tsx ์ฌ์ฉ ์์ ์ฝ๋
๊ฒฐ๋ก
๋น์ฐํ๊ฒ ์ง๋ง ํ์ฌ ์ฐจ์ฉํ๊ณ ์๋ ๋ฐฉ์ ๋ํ ์๋ฒฝํ ์ ์๊ณ , ์ฅ๋จ์ด ์กด์ฌํฉ๋๋ค
์ธ์ ๋ฐฉ์์ ์๋ฒ์ ๋ถ๋ด์ด ๋ง์ด ๊ฐ๊ธฐ ๋๋ฌธ์ ๋ง๋ฃ๋๋ ์ฃผ๊ธฐ๊ฐ ์งง์ ์ ์ ๊ฐ ๋ค์ ๋ก๊ทธ์ธ์ ์ํํด์ผํ๋ ์ฃผ๊ธฐ๊ฐ ์งง์์ก์ผ๋ฉฐ, ์ธ์ ์ด ๋ง๋ฃ๋์์ ๋์ ์๋ฌ์ฒ๋ฆฌ ๋ํ ์ฝ์ง๋ง์ ์์์ต๋๋ค
๋ํ ๊ฑฐ์ ๋ชจ๋ ์ ์ ๊ด๋ จ API ํต์ ๋ก์ง์ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ์์ ์ฒ๋ฆฌํด์ผ ํ์ต๋๋ค
๋ ๋์ ์ธ์ฆ ๋ฐฉ์์ ์๊ณ ์๊ฑฐ๋ ๋ค๋ฅธ ์๊ฒฌ์ด ์๋ค๋ฉด ๊ณต์ ํด ์ฃผ์๊ธธ ๋ถํ๋๋ฆฌ๊ฒ ์ต๋๋ค