์ ๋ฒ ์ฃผ๋ถํฐ ์ด๋ฒ ์ฃผ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ๋ชจ๋ ๋ฐฑ์๋ ๊ด๋ จ์ด์๋ค.
์๋ฒ ์ฝ๋๋ฅผ ๊ตฌํํ ๋ ๋น์ ์ด ์ฑํฐ๋ง ๋๊ธฐ๋ฉด ๋ค์ ํ๋ก ํธ๋ฅผ ๋ฐฐ์ฐ์ง ์์๊น?
ํ๋ ์๊ฐ์ด ์์์ง๋ง ์ ์ ์ฐจ๋ ค๋ณด๋ ๋์ฑ๋ ์ฌํ์ ์ธ ๋ฐฑ์๋๋ฅผ ๋ฐฐ์ฐ๊ณ ์์๋ค.
ใ
ใ
ใ
ใ
ใ
ใ
...
๋์์ง๋ ์์ง๋ง, ํ๋ก ํธ์๋๋ฅผ ๋นจ๋ฆฌ ๋ฐฐ์ฐ์ง ์์ผ๋ฉด ๊ฐ๋ ์์ด๋ฒ๋ฆด ๊ฒ ๊ฐ์๊ณ
๋ฌด์๋ณด๋ค ์ด๋ฌ๋ค๊ฐ ๊ทธ๋ฅ ํ์คํ
์ ํด๋ฒ๋ฆฌ๋ ๊ฑฐ ์๋์ผ? ํ๋ ์ธ๋ฐ์๋ ๊ฑฑ์ ๋ ๋์๋ค.
๋ ธ๋ง๋์ฝ๋ Next.JS ๊ฐ์๋ ๊ด์ค๋ ๋ฃ๊ณ ์๋ 2์ฃผ์๋ค.
๊ทธ๋์ ์ค๋ ์ค๋ ์งํํ๋ ๊ฒ์ ์๋๋ฏ๋ก, ์์
๋ค์ผ๋ฉด์ ๋จ๊ฒจ๋จ๋
์ธ์ธํ ๊ธฐ๋ก๋ค์ ์ด์ ํ์ด๋ณด๋๋ก ํ๊ฒ ๋ค! ํ์ฐ ๐จ๐จ
๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ
ํด๋ผ์ด์ธํธ : ์๋ฒ๋ก ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ด๊ธฐ
a. ์์ด๋, ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํด์ ๋ก๊ทธ์ธ ๋ฒํผ์ ํด๋ฆญํ๋ค.
์๋ฒ : ๋ก๊ทธ์ธ ์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ
a. ์์ฒญ์ผ๋ก ์ ๋ฌ๋ฐ์ ์์ด๋์ ๋น๋ฐ๋ฒํธ๊ฐ ํ์๊ฐ์
์ด ๋์ด์๋ ์ ๋ณด์ธ์ง ํ์ธํ๋ค.
b. ํ์์ด๋ผ๋ฉด, ์๋ต์ ์ฟ ํค์ ํ์ ์ ๋ณด๋ฅผ ๋ด์ ์ ๋ฌํ๋ค.
ํด๋ผ์ด์ธํธ : ์๋ต์ ๋ฐ์ React ์ํ๋ฅผ ์ ๋ฐ์ดํธ
ํด๋ผ์ด์ธํธ : ํ๋ฉด ๋ฆฌ๋ ๋๋ง โ ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ผ๋ก ๋ง์ดํ์ด์ง ํ์
๋ง์ฝ ๋ก๊ทธ์ธํ ๋ โ๋ก๊ทธ์ธ ์ ์งโ ์ต์
์ ์ ํํ๋ค๋ฉด, ํด๋น ํ์ด์ง์ ์ฌ์ ์ํ ๋ ๋ค์ ๋ก๊ทธ์ธํ ํ์ ์์ด ์๋ฒ์์ ๋ฐ์์จ ์ฟ ํค๋ฅผ ์ฌ์ฉํด์ ํ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค๊ฒ ๋ฉ๋๋ค.
๋ก๊ทธ์์์ ์ผ๋ถ๋ฌ ํ์ง ์๋ ์ด์, ์ฟ ํค ์ ์ง ์๊ฐ๋งํผ ์๋ ๋ก๊ทธ์ธ์ด ์ ์ง๋ฉ๋๋ค.
ํด๋ผ์ด์ธํธ : ๋ก๊ทธ์์ ๋ฒํผ์ ๋๋ฌ ์๋ฒ์ ๋ก๊ทธ์์ ์์ฒญ ๋ณด๋ด๊ธฐ
์๋ฒ : ๋ก๊ทธ์์ ์์ฒญ ์ฒ๋ฆฌ
a. ์ฟ ํค๋ฅผ ์ญ์ ํ๊ณ ํด๋ผ์ด์ธํธ๋ก ์๋ต์ ๋ณด๋ธ๋ค.
ํด๋ผ์ด์ธํธ : ์๋ต์ด ์ ๋์์ค๋ฉด React ์ํ ์ด๊ธฐํ
ํด๋ผ์ด์ธํธ : ํ๋ฉด ๋ฆฌ๋ ๋๋ง โ ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ผ๋ก ๋ก๊ทธ์ธ ํ์ด์ง ํ์
1. ํด๋ผ์ด์ธํธ : ์๋ฒ๋ก ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ด๊ธฐ
ํด๋ผ์ด์ธํธ์ Login.js ํ์ผ์ ํ์ธํด ๋ณด์ธ์.
์ํ๊ฐ 3๊ฐ์ง๊ฐ ์์ต๋๋ค.
์ผ๋จ ์์ด๋, ๋น๋ฐ๋ฒํธ ์ค ํ๋๋ผ๋ ์ ๋ ฅ์ด ๋์ง ์์๋ค๋ฉด ์๋ฌ๋ฅผ ๋์ฐ๋๋ก ์ฝ๋๋ฅผ ์์ฑํด ์ค๋๋ค.
const loginRequestHandler = () => {
if (!loginInfo.userId || !loginInfo.password) {
setErrorMessage('์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์')
// ์
๋ ฅ๋์ง ์์ ๊ฐ์ด ์๋๊ฑฐ๋๊น ์์ฒญ์ ๋ณด๋ด๋ณผ ํ์๋ ์์ด ๋ฐ๋ก ๋ฆฌํดํด์ค๋๋ค.
return;
}
...
}
Axios ์์ฒญ์ ๋ณด๋ผ ๋, ๋ก๊ทธ์ธ์ ์ฒ๋ฆฌํ๋ ์๋ํฌ์ธํธ๋ฅผ ์
๋ ฅํด ์ค๋๋ค. โ "http://localhost:4000/login"
req.body
์์ loginInfo
, checkedKeepLogin
๋ฅผ ํ์ธํ๊ณ ์์ผ๋ Login.js์ ์ํ๋ฅผ ๊ทธ๋๋ก ์ค์ด์ ๋ณด๋ด์ค์๋ค.req.body
๋ ์ด๋ป๊ฒ ๋ณด๋ด๋์? Axios๋ก post ์์ฒญ์ ๋ณด๋ผ ๋์๋ ์๋ํฌ์ธํธ ๋ค์ ์ธ์๋ก ๋ฃ์ด์ฃผ๋ฉด ๋ฉ๋๋ค..post("http://localhost:4000/login", {loginInfo, checkedKeepLogin})
์๋ฒ์์ ์์ฒญ์ ์ ๋ฐ์์ค๋์ง ํ์ธํด ๋ด ์๋ค. (npm start๋ก ์๋ฒ๋ฅผ ์ด์ด๋์์ผ ์ ๋๋ก ํ์ธํ ์ ์์ต๋๋ค.)
console.log(req.body)
๋ฅผ ์์ฑํ๊ณ ์ ์ฅํ ๋ค์ ๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ฌ๋ณด์ธ์.์์ฒญ์ด ์ ๋ค์ด์๋ค๋ฉด, ๋ค์ด์จ ์ ๋ณด๋ฅผ ํ์ฉํ์ฌ ์์ฒญ์ ์ฒ๋ฆฌํ ๋ค์ ์๋ต์ ๋ณด๋ด์ฃผ๋๋ก ํฉ์๋ค.
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
204 ์์ฒญ๊น์ง ์ ๋จ๊ณ , req.body๊น์ง ์ ์์ ์ผ๋ก ๋จ๋ ๊ฒ์ ํ์ธํ ์ ์์๋ค.
์์ง์ ๋ก๊ทธ์ธ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋ก์ง์ ๊ตฌํํ์ง ์์์, ๋ง์ดํ์ด์ง ํ๋ฉด์ ์ด๋ํ์ง ์๋๋ค.
2. ์๋ฒ : ๋ก๊ทธ์ธ ์์ฒญ์ ์ฒ๋ฆฌํ๊ธฐ
โ๏ธ /login.js
ํ์ผ ์์ ์์ฑ๋ ์ฃผ์์ ๋ฐ๋ผ์ ๋ก์ง์ ์ฐจ๊ทผ์ฐจ๊ทผ ์์ฑํด ๋ด ์๋ค. ์ผ๋จ userInfo๋ผ๋ ๋ณ์๊ฐ ๋ฌด์์ธ์ง ์์๋ณผ๊น์?
const userInfo = {
...USER_DATA.filter((user) => user.userId === userId && user.password === password)[0],
};
์ง์ ํ์ธํด ๋ณผ๊น์? console.log(userInfo)๋ฅผ ์ถ๊ฐ๋ก ์์ฑํ ํ์ ๋ก๊ทธ์ธ ์์ฒญ์ ๋ณด๋ด๋ณด์ธ์.
if (userInfo.id === undefined) { ... }
๋ก๊ทธ์ธ ์คํจ ์๋ต ์ฝ๋๋ ๊ตฌ์ฒด์ ์ผ๋ก ์ด๋ป๊ฒ ์์ฑํ๋ฉด ์ข์๊น์? ํ ์คํธ ์ผ์ด์ค๋ฅผ ํ์ธํด ๋ณด์ธ์.
// ํ
์คํธ ์ผ์ด์ค์ ๋ง์ถฐ์ ์ฝ๋๋ฅผ ์์ฑํด ์ค๋๋ค.
if (userInfo.id === undefined) {
res.status(401).send('Not Authorized')
}
๋ก๊ทธ์ธ์ ์ฑ๊ณตํ๋ค๋ฉด, ํด๋ผ์ด์ธํธ์ ์ฟ ํค๋ฅผ ์ ์กํด์ผ ํฉ๋๋ค. ์ฃผ์์ ์ฝ์ด๋ณด๋ cookieId
์ userInfo.id
๋ฅผ ๋ด์์ผ ํ๋ค๊ณ ์ค๋ช
๋์ด ์์ต๋๋ค.
res.cookie
๋ฉ์๋๋ ์ ๋ฌ์ธ์๋ก ์ฟ ํค ์ด๋ฆ, ์ฟ ํค ๊ฐ, ์ฟ ํค ์ต์
์ ๋ฐ์ต๋๋ค.์ฌ๊ธฐ์ ์ฟ ํค ์ต์ ์ ๊ฐ๋ ํ์ต์์ ๋ฐฐ์ด ๋ด์ฉ์ ๋๋ค. (๋งํฌ) ๊ฐ๋ ํ์ต์ ๋ด์ฉ์ ์์๋๋ก ๋ณด๋ฉด์ ํ๋์ฉ ์์ฑํด ๋ณด๊ฒ ์ต๋๋ค.
{
domain: 'localhost',
path: '/',
// domain๊ณผ path ์ต์
์ ์ฟ ํค๋ฅผ ์ ๋ฌํ ๊ฒฝ๋ก๋ฅผ ์ง์ ํด์ค๋๋ค.
// ์๋ฑํ ๊ณณ์ผ๋ก๋ ์ฟ ํค๋ฅผ ๋ณด๋ด๊ณ ์ถ์ง ์๋ค๋ฉด, ์ ์์ฑํด์ฃผ๋๋ก ํฉ์๋ค.
// MaxAge, Expires ์ต์
์ ๋ก๊ทธ์ธ ์ ์ง ์ฒดํฌ ์ฌ๋ถ์ ๋ฐ๋ผ ๋ฌ๋ผ์งํ
๋ ์ผ๋จ ํจ์คํฉ๋๋ค.
secure: true,
// ์ง๊ธ์ ์๋ฒ์ ํด๋ผ์ด์ธํธ ๋ชจ๋ localhost๋ผ ์์ฑํ์ง ์์๋ ์๊ด ์์ง๋ง,
// ๊ฐ๋ฅํ๋ฉด secure ์ต์
์ true๋ก ์ค์ ํ๋๋ก ํฉ์๋ค.
// https๋ฅผ ํตํด ์ฟ ํค๋ฅผ ์ฃผ๊ณ ๋ฐ์์ผ ์ฟ ํค๊ฐ ์ํธํ ๋๊ธฐ ๋๋ฌธ์
๋๋ค.
httpOnly: true,
// httpOnly ์ต์
์ ๋ฌด์กฐ๊ฑด true๋ก ์ค์ ํด์ฃผ๋ ๊ฒ์ด ์ข์ต๋๋ค.
// false์ผ ๊ฒฝ์ฐ ์ฟ ํค๊ฐ ํ์ทจ๋ ์ํ์ด ์๊ธฐ๊ธฐ ๋๋ฌธ์
๋๋ค.
sameSite: 'strict',
// ์๋ฒ๋ http://localhost:4000, ํด๋ผ์ด์ธํธ๋ http://localhost:3000์ ์ฌ์ฉํฉ๋๋ค.
// ์ด ๋, ๋ ์ฃผ์๋ TLD(.com, .org ...)๋ฅผ ๊ฐ๊ณ ์์ง ์์ eTLD+1๋ ์์ง๋ง,
// ๋์ผํ host(localhost)๋ฅผ ๊ฐ๊ธฐ ๋๋ฌธ์ sameSite๋ก ํ๋จํฉ๋๋ค.
// ๋ ์ฃผ์๊ฐ sameSite์ด๊ณ , ์ฟ ํค๋ก ๋ด์์ค ์ ๋ณด๊ฐ ๋ก๊ทธ์ธ ๊ด๋ จ ์ ๋ณด์ธ ๋งํผ,
// sameSite ๋ด์์๋ง ์ฟ ํค๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋๋ก strict๋ก ์ค์ ํด์ค์๋ค.
}
cookiesOption
์ด๋ผ๋ ์ด๋ฆ์ ๊ฐ์ฒด๋ก ๋ง๋ ๋ค์ ์ ๋ฌ์ธ์๋ก ๋ฃ์ด์ฃผ๊ฒ ์ต๋๋ค.if (userInfo.id === undefined) {
...
} else {
res.cookie('cookieId', userInfo.id, cookiesOption)
}
์ฃผ์์ ๋ณด๋ฉด ์์์ฑ ์๋ ์ฟ ํค๋ฅผ ๋ณด๋ด๋ ค๋ฉด max-age
๋๋ expires ์ต์
์ ์ค์ ํ์ธ์
๋ผ๋ ์ค๋ช
์ด ์์ต๋๋ค. ์ด๋ ๋ก๊ทธ์ธ์ ์ ์งํ๊ณ ์ถ์ผ๋ฉด ํด๋น ์ต์
์ ์์ฑํ๋ผ๋ ๋ป์
๋๋ค. ์ด ์ต์
์ ์์ฑํ์ง ์์ผ๋ฉด ๋ธ๋ผ์ฐ์ ๋ฅผ ๊บผ๋ฒ๋ฆฌ๋ฉด ์ฟ ํค๊ฐ ์ฌ๋ผ์ง๋๋ค.
๋ก๊ทธ์ธ ์ฑ๊ณต ๋ก์ง์ checkedKeepLogin
์ผ๋ก ํ ๋ฒ ๋ ๋ถ๊ธฐํ๊ณ , true
์ผ ๊ฒฝ์ฐ(๋ก๊ทธ์ธ์ ์ ์งํ ๊ฒฝ์ฐ)์ ํด๋น ์ต์
์ ์ถ๊ฐ๋ก ์์ฑํด ์ฃผ์ธ์.
...
} else if (checkedKeepLogin === true) {
// ๋ก๊ทธ์ธ์ ์ ์งํ๊ณ ์ถ์ ๊ฒฝ์ฐ, cookiesOption์ max-age ๋๋ expires ์ต์
์ ์ถ๊ฐ๋ก ์ค์ ํด์ฃผ๊ฒ ์ต๋๋ค.
// max-age ์ต์
์ผ๋ก ์์ฑํ๋ ๊ฒฝ์ฐ
cookiesOption.maxAge = 1000 * 60 * 30
// ๋จ์๋ ms(๋ฐ๋ฆฌ์ธ์ปจ๋ === 0.001์ด)์ด๋ ์ฃผ์ํ์ธ์! -> ์ด๋ ๊ฒ ์์ฑํ ๊ฒฝ์ฐ 30๋ถ๋์ ์ฟ ํค๋ฅผ ์ ์งํฉ๋๋ค.
// expires ์ต์
์ผ๋ก ์์ฑํ๋ ๊ฒฝ์ฐ์๋, ์ด๋ ๊ฒ ์์ฑํ์๋ฉด ๋ฉ๋๋ค.
cookiesOption.expires = new Date(Date.now() + (1000 * 60 * 30) )
// ์ง๊ธ ์๊ฐ + 30๋ถ ํ์ ์ฟ ํค๋ฅผ ์ญ์ ํ๋ค๋ ์๋ฏธ์
๋๋ค.
res.cookie('cookieId', userInfo.id, cookiesOption)
} else {
// ๋ก๊ทธ์ธ์ ์ ์งํ๊ณ ์ถ์ง ์์ ๊ฒฝ์ฐ, max-age ๋๋ expires ์ต์
์ ์์ฑํ์ง ์์ ์ํ ๊ทธ๋๋ก ์ฟ ํค๋ฅผ ์ค์ ํฉ๋๋ค.
res.cookie('cookieId', userInfo.id, cookiesOption)
}
์ฌ๊ธฐ๊น์ง ์์ฑํ๊ณ ์ฃผ์์ ํ์ธํด ๋ณด๋ฉด, ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐ๋ก ์๋ต์ ๋ณด๋ด์ง ์๊ณ ์๋ฒ์ /useinfo ์๋ํฌ์ธํธ๋ก ๋ฆฌ๋ค์ด๋ ํธ ์์ผ์ฃผ๋ผ๊ณ ํฉ๋๋ค.
redirect
๋ฉ์๋์ ์๋ํฌ์ธํธ๋ฅผ ์ ๋ฌํด ์ฃผ๋ฉด ๋ฉ๋๋ค. โ res.redirect("/userinfo")
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
์ฒ์์ ์์ด๋๋ฅผ ์๋ฌด๊ฑฐ๋ ์น๊ณ ๋ก๊ทธ์ธ ์๋๋ฅผ ํ์๋๋ฐ, ์ ์์ ์ผ๋ก userinfo๊ฐ ๋ณด์ด์ง ์์์ ๋นํฉํ๋ค.
์๊ณ ๋ณด๋ ํ์ฌ๋ก์๋ ๋ชจ์ ๋ฐ์ดํฐ๋ก ๊ตฌํํ๋ ๊ฒ์ด๋ฏ๋ก, data.js ์์ ์๋ ์ ํด์ง ๊ฐ์ ์ ํด์ค์ผ ํ๋ค.
๋ค์๊ณผ ๊ฐ์ด ์์ด๋์ ๋น๋ฒ์ ์
๋ ฅํด์ฃผ๋ฉด, ์ ์์ ์ผ๋ก ๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ ์ ์,
ํฐ๋ฏธ๋์์ userinfo๊ฐ ์ ์์ ์ผ๋ก ์ ๋ฌ๋๋ ๊ฒ์ ๋ณผ ์ ์์๋ค.
๊ทธ๋ฆฌ๊ณ ์ธ๊ธ๋ ๋ฆฌ๋ค์ด๋ ํธ์ ๋ํด์๋ ์๋ Error note์์ ๋ค๋ฃจ๊ฒ ๋ค.
โ๏ธ /login.js
์ฟ ํค๋ฅผ ๊ฒ์ฆํด์ ์ ์ ์ ๋ณด๋ฅผ ์ ๋ฌํด ์ค์๋ค.
console.log(req.cookies)
๋ก ์ฟ ํค๋ฅผ ํ์ธํ ์ ์์ต๋๋ค. login.js์ ์๋ ํํฐ๋ง ๋ก์ง์ ๊ทธ๋๋ก ๊ฐ์ ธ์์ ์ฌํ์ฉํ๊ฒ ์ต๋๋ค.
const cookieId = req.cookies.cookieId
const userInfo = {
...USER_DATA.filter((user) => user.id === cookieId)[0],
};
user.id
๋ก ํํฐ๋งํด์ผ ํฉ๋๋ค. ํท๊ฐ๋ฆฌ๊ธฐ ์ฝ์ต๋๋ค!id
๊ฐ ์๋ ๊ฒฝ์ฐ userInfo
๊ฐ ๋น์ด์์ ์๋ ์์ต๋๋ค.userInfo
๊ฐ์ด ์ ๋๋ก ๋ค์ด์ค์ง ์์ต๋๋ค.if ( !cookieId || !userInfo.id )
๋ก ์์ฑํฉ๋๋ค.์๋ต ์ฝ๋ ์์ฑํ๊ธฐ
if (!cookieId || !userInfo.id){
res.status(401).send('Not Authorized');
} else {
// ๋น๋ฐ๋ฒํธ๋ ๋ฏผ๊ฐํ ์ ๋ณด๋ผ์ ์ญ์ ํ์ ๋ณด๋ด์ผ ํฉ๋๋ค.
delete userInfo.password
res.send(userInfo)
}
์ด์ ์๋ฒ์์ ์๋ตํ๋ ์ฝ๋๋ฅผ ์์ฑํ์ต๋๋ค. ํด๋ผ์ด์ธํธ์์ ์ ๋ค์ด์ค๋์ง ํ์ธํด ๋ณด๊ฒ ์ต๋๋ค.
console.log(res.data)
๋ฅผ ์ถ๊ฐํฉ๋๋ค.return axios
.post(...)
.then((res) => {
// ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐ์์จ ๋ค์ ํ์ธํด์ผํ๋ฏ๋ก ์ด ์์น์์ ์์ฑํฉ๋๋ค.
console.log(res.data)
})
...
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
์์ ๊ฐ์ด ์์ฑ ์, ์ ์์ ์ผ๋ก login ๋ก์ง(์ฟ ํค ๋ฐ๊ธ) ์ฒ๋ฆฌ ํ,
redirect๋ก ์ธํด userinfo ๊น์ง ๋ก์ง์ด ๊ตฌํ๋๋ ๊ฒ์ ์ ์ ์๋ค.
๋ํ ์ฝ์์ฐฝ์์ ์ ์์ ์ผ๋ก ์๋ต๋ฐ๊ณ ์๋ ๊ฐ์ ํ์ธํ ์ ์์๋ค.
3. ํด๋ผ์ด์ธํธ : ์๋ต์ ๋ฐ์ React ์ํ๋ฅผ ์ ๋ฐ์ดํธ
์ฐ์ ํด๋ผ์ด์ธํธ์ App.js์์ Login.js๋ก ํ์ํ props๋ฅผ ๋ด๋ ค์ฃผ์ธ์. (setIsLogin
, setUserInfo
)
๋ฐ์์จ ์ํ ๋ณ๊ฒฝ ํจ์๋ก ์ํ๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค.
setIsLogin(true)
: ๋ก๊ทธ์ธ๋์์์ ์ํ๋ก ์ ์ฅํ๊ธฐsetUserInfo(res.data)
: ๋ก๊ทธ์ธํ ํ์์ ์ ๋ณด๋ฅผ ์ํ์ ์ ์ฅํ๊ธฐcatch๋ฅผ ํตํด ์๋ฌ ์ฒ๋ฆฌ๋ ํด์ฃผ๊ฒ ์ต๋๋ค. ์๋ฌ๋ console.log(err.response.data)
๋ก ํ์ธํ๊ณ , ์๋ฌ๋ฉ์์ง๋ setErrorMessage(โ๋ก๊ทธ์ธํ ์ ์์ต๋๋ค")
๋ก ์ค์ ํฉ๋๋ค.
์๋ฌ ๋ฉ์์ง๊ฐ ๊ณ์ ๋จ์ ์์ผ๋ฉด ์ ๋๋๊น ๋ก๊ทธ์ธ ์ฑ๊ณตํ์ ๊ฒฝ์ฐ์๋ ์ด๊ธฐํ๋ ํด ์ฃผ์ธ์.
๊ผญ ์๋์ฒ๋ผ ์ด๊ธฐํํ ํ์๋ ์์ต๋๋ค. ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ํด์ค ๋(์์ด๋, ๋น๋ฐ๋ฒํธ๊ฐ ์ ๋ ฅ๋์ด ์๋์ง ์๋์ง ํ์ธํ ๋) ํด์ฃผ๋ ๊ฒ๋ ํ ๋ฐฉ๋ฒ์ ๋๋ค.
return axios
.post("http://localhost:4000/login", {loginInfo, checkedKeepLogin})
.then((res) => {
setUserInfo(res.data)
setIsLogin(true)
//์ฌ๊ธฐ์์ ์๋ฌ ์ด๊ธฐํ
setErrorMessage("")
})
.catch((err) => {
console.log(err.response.data)
setErrorMessage("๋ก๊ทธ์ธ์ ์คํจํ์ต๋๋ค.")
});
์ฌ๊ธฐ๊น์ง ์์ฑํ์ผ๋ฉด Login.js๋ ์์ฑ์ ๋๋ค!
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
์ด์ ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ๊ตฌํํ์ผ๋ฏ๋ก, ๋ก๊ทธ์ธ ๋ฒํผ์ ๋๋ฅด๋ฉด ํ๋ฉด์ด ๋์ด๊ฐ๊ธฐ๋ ํ๋ค.
ํ์ง๋ง ์์ง Mypage.js ๊ตฌํ์ด ์๋ฃ๋์ง ์์์ผ๋ฏ๋ก ๋น์ฐํ ์๋ฌ๊ฐ ๋ฌ๋ค.
4. ํด๋ผ์ด์ธํธ : ํ๋ฉด ๋ฆฌ๋ ๋๋ง โ ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ผ๋ก ๋ง์ดํ์ด์ง ํ์
App.js์์ Mypage์ props๋ก userInfo
๋ฅผ ๋ด๋ ค์ฃผ์ง ์์ ๊ฒฝ์ฐ ์ค๋ฅ๊ฐ ๋ฐ์ํฉ๋๋ค. props๋ฅผ ์ ๋๋ก ๋ด๋ ค์ฃผ์ธ์!
userInfo
๋ฅผ ๋ด๋ ค์ค ํ ๋ค์ ๋ก๊ทธ์ธํ๋ฉด ์๋์ ๊ฐ์ด Mypage๊ฐ ์ ๋๋ก ํ์๋ฉ๋๋ค.
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
์ด์ ์ ์์ ์ผ๋ก ๋ง์ดํ์ด์ง๊ฐ ํ๋ฉด์ ๋ณด์ธ๋ค !
์์ง ๊ตฌํ์ด ํ๋ ๋จ์๋ค.
5. ํด๋ผ์ด์ธํธ : ๋ก๊ทธ์์ ๋ฒํผ์ ๋๋ฌ ์๋ฒ์ ๋ก๊ทธ์์ ์์ฒญ ๋ณด๋ด๊ธฐ
.post("http://localhost:4000/logout")
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
๋ก๊ทธ์์ ์๋ํฌ์ธํธ๋ฅผ ์ค์ ํด์ค ๊ฒ์ฒ๋ผ ํฐ๋ฏธ๋์ ํ์ธํ๋ฉด logout์ ๋ณผ ์ ์๋ค !
ํ์ง๋ง ์ฝ๋ ๊ตฌํ์ด ์์ง ๋๋์ง ์์๊ธฐ ๋๋ฌธ์ ์ค๋ฅ๊ฐ ๋๋ค.
6. ์๋ฒ : ๋ก๊ทธ์์ ์์ฒญ ์ฒ๋ฆฌ
โ๏ธ /logout.js
res.clearCookie
๋ฉ์๋๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.cookieId
, ์ต์
์ login.js์์ ๋ง๋ค์ด์ค cookiesOption
๊ฐ์ฒด๋ฅผ ๊ฐ์ ธ์์ ์ฐ๋ฉด ๋ฉ๋๋ค.const cookiesOption = {
domain: 'localhost',
path: '/',
secure: true,
httpOnly: true,
sameSite: 'strict',
}
res.status(205).clearCookie('cookieId', cookiesOption).send("logout")
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
์ด์ ์ ์์ ์ผ๋ก ์ฟ ํค๊ฐ ์ญ์ ๋์๊ณ , ํฐ๋ฏธ๋์ ํ์ธํด๋ณด๋ฉด ์์ฒญ๋ ์ ์์ ์ผ๋ก ์ฒ๋ฆฌ๋๋ค.
7. ํด๋ผ์ด์ธํธ๋ ์ํ๋ฅผ ๋น์ด๋ค.
setIsLogin(false)
: ๋ก๊ทธ์์ ๋์์์ ์ํ๋ก ์ ์ฅํ๊ธฐsetUserInfo(null)
: ์ ์ฅํด ๋์๋ ํ์์ ์ ๋ณด๋ฅผ ๋น์ฐ๊ธฐconsole.log(err.response.data)
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
์ด์ ๋ก๊ทธ์์ ๋ฒํผ์ ๋๋ฅผ ์, ๋์์ ์ ์์ ์ผ๋ก ํํ์ด์ง๋ก ์ด๋ํ๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
8. ์กฐ๊ฑด๋ถ ๋ ๋๋ง์ผ๋ก ๋ก๊ทธ์ธ ํ์ด์ง๊ฐ ํ์๋๋ค.
์ด์ ๋ก๊ทธ์์ ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ๋ค์ ์ค๊ฒ ๋ฉ๋๋ค. ๊ทธ๋ฐ๋ฐ, ๋ก๊ทธ์ธ ์ ์ง ์ต์ ์ ์ ํํ๊ณ ๋ก๊ทธ์ธํด๋, ๋ธ๋ผ์ฐ์ ๋ฅผ ๋๊ณ ๋ค์ ์ ์ํ์ ๋ ๋ก๊ทธ์ธ์ด ์ ์ง๊ฐ ๋์ง ์๋ ๊ฒ์ ํ์ธํ ์ ์์ต๋๋ค.
๋ก๊ทธ์ธ ์ ์ง ๋ก์ง์ด ์์ฑ๋์ด์์ง ์๊ธฐ ๋๋ฌธ์ ๋๋ค. App.js๋ฅผ ํ์ธํด ๋ด ์๋ค.
useEffect
๊ฐ ํ์ด์ง๋ฅผ ๋ ๋๋งํ ๋๋ง๋ค ์ต์ด 1๋ฒ์ฉ authHandler
๋ฅผ ์คํ์ํค๊ณ ์๋ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.์ฟ ํค๋ฅผ ํ์ธํ๊ณ ์ผ์นํ๋ ์ ์ ์ ๋ณด๋ฅผ ๋ณด๋ด์ฃผ๋ ์๋ํฌ์ธํธ๋ ์ด๋์ธ๊ฐ์? โ /userinfo
return axios
.get("http://localhost:4000/userinfo")
.then((res) => {
setIsLogin(true)
setUserInfo(res.data)
})
.catch((err) => {
console.log(err.response.data)
});
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
๐ก res.redirect
res.redirect๋ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ฌ์ฉ์๋ฅผ ๋ค๋ฅธ URL๋ก ๋ฆฌ๋๋ ์ ํ๋ ๋ฐ ์ฌ์ฉ๋๋ Node.js์ Express ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ํจ์์ด๋ค. ์ด ํจ์๋ ํด๋ผ์ด์ธํธ์ ์์ฒญ์ ๋ค๋ฅธ URL๋ก ๋ณด๋ด๊ณ , ํด๋น URL์์ ์๋ก์ด ํ์ด์ง๋ฅผ ๋ก๋ํ๋๋ก ์ ๋ํ๋ค.
res.redirect ํจ์๋ ๋ค์๊ณผ ๊ฐ์ ๋ชฉ์ ์ผ๋ก ์ฌ์ฉ๋ ์ ์๋ค:
res.redirect ํจ์์ ํ์์ฑ์ ๋ค์๊ณผ ๊ฐ๋ค:
์์ฝํ์๋ฉด, res.redirect ํจ์๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์์ ์ฌ์ฉ์๋ฅผ ๋ค๋ฅธ URL๋ก ๋ณด๋ด๋ ๋ฐ ์ฌ์ฉ๋๋ฉฐ, ์ฌ์ฉ์ ๊ฒฝํ, ๋ณด์ ๋ฐ ์ธ์ฆ, ๊ทธ๋ฆฌ๊ณ SEO ๊ด๋ จ ๋ชฉ์ ์ ์ํด ํ์ํฉ๋๋ค.
๐ก props๋ฅผ ์ ํํ๊ฒ ์ ๋ฌํ์ !
// โ
์ฌ๋ฐ๋ฅธ ์์
export default function Mypage({ userInfo, setIsLogin, setUserInfo }) {
const logoutHandler = () => {
// TODO: Logout ๋ฒํผ์ ๋๋ ์ ์ Login ํ์ด์ง๋ก ๋์๊ฐ ์ ์๋๋ก ๊ตฌํํ์ธ์.
return axios
.post("http://localhost:4000/logout")
.then((res) => {
setIsLogin(false);
setUserInfo(null);
})
.catch((err) => {
console.log(err.response.data);
});
};
// โ ํ๋ฆฐ ์์
export default function Mypage({ userInfo}) {
const logoutHandler = ({setIsLogin, setUserInfo}) => {
// TODO: Logout ๋ฒํผ์ ๋๋ ์ ์ Login ํ์ด์ง๋ก ๋์๊ฐ ์ ์๋๋ก ๊ตฌํํ์ธ์.
return axios
.post("http://localhost:4000/logout")
.then((res) => {
setIsLogin(false);
setUserInfo(null);
})
.catch((err) => {
console.log(err.response.data);
});
};
์ด๋ฒ ๊ณผ์ ๋ ์ฟ ํค ํํ ๋ฆฌ์ผ์์ ์งํํ๋ ํ๋ฆ์์ ์๋ฒ๊ฐ ์ฟ ํค ๋์ ์ธ์ ์ ์ด์ฉํ๋๋ก ๋ฐ๊ฟ์ฃผ๋ฉด ๋ฉ๋๋ค. ํด๋ผ์ด์ธํธ๋ ์ฟ ํค ํํ ๋ฆฌ์ผ์ ์ฝ๋๋ฅผ ๊ทธ๋๋ก ๊ฐ์ ธ๋ค ์ฌ์ฉํ์ธ์.
์ธ์ ๋ ์ฟ ํค๋ฅผ ์ฌ์ฉํ๊ธด ํ์ง๋ง, ์ฟ ํค๋ก ๊ตฌํํ๋ ๊ฒ์ฒ๋ผ ์ ์ id๋ฅผ ์ง์ ๋ด๋ ๊ฒ ์๋๋ผ, ์๋ฒ์์ ์ธ์ id๋ฅผ ๋ฐ์์์ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค.
CORS ์ค์ ์ ์์ง ๋ง๊ณ ํด ์ฃผ์ธ์! ์ฟ ํค ๋๋ ๋๊ฐ์ด ํด์ฃผ์๋ฉด ๋ฉ๋๋ค.
const corsOptions = {
origin: "http://localhost:3000",
credentials: true,
methods: ['GET', 'POST', 'OPTION']
};
โ๏ธ /login
์ฟ ํค ํํ ๋ฆฌ์ผ ๋์ ๋ก์ง์ ๋๊ฐ์ต๋๋ค. ์ธ์ ์ ์ฐ๋ ์ฟ ํค๋ฅผ ์ฐ๋ ์ฐจ์ด์ผ ๋ฟ์ด๋ผ, ์กฐ๊ฑด๋ฌธ ๋ถ๊ธฐ๋ ๋๊ฐ์ด ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
ํด๋ผ์ด์ธํธ์ ์ธ์ id๋ฅผ ์ ์ฅํด ๋ด ์๋ค.
req.session
์ ์ฌ์ฉํด ์ธ์
๊ฐ์ฒด์ userInfo.id
๋ฅผ ์ ์ฅํ์ธ์.
// ์ด๋ฒ์๋ ์ฟ ํค ํํ ๋ฆฌ์ผ๊ณผ ๋ค๋ฅด๊ฒ res๊ฐ ์๋๋ผ req์ ์์ฑํด์ผ๋ฉ๋๋ค. ์ ํด์ง ์ฌ์ฉ๋ฒ์ด๋ ์ฃผ์!!
req.session.sessionId = userInfo.id
์ด์ maxAge๋ฅผ ์ค์ ํด์ฃผ๊ณ ์ถ์๋ฐ, ์ธ์ ์ ์ฟ ํค ์ต์ ์ ์ด๋ป๊ฒ ์ค์ ํด ์ค ์ ์์๊น์?
console.log(req.session)์ผ๋ก ์ธ์ ์ ๊ตฌ์กฐ๋ฅผ ํ์ธํด ๋ณด์ธ์.
req.session.cookie ์์ ๋ฃ์ด์ฃผ๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
์ต์ ์ ๋ฃ์ด์ฃผ๊ณ redirect๊น์ง ์์ฑํด ์ค๋๋ค.
if (!userInfo.id) {
res.status(401).send("Not Authorized")
} else if (checkedKeepLogin) {
req.session.sessionId = userInfo.id
// ์ธ์
๊ตฌ์กฐ ๋ค์ฌ๋ค๋ณด๊ธฐ -> ํฐ๋ฏธ๋์ ํ์๋ฉ๋๋ค
console.log(req.session)
// ์ฟ ํค ์ต์
์ค์ ํ๊ธฐ
req.session.cookie.maxAge = 1000 * 60 * 30
res.redirect('/userinfo')
} else {
req.session.sessionId = userInfo.id
res.redirect('/userinfo')
}
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
ํด๋ผ์ด์ธํธ๋ ์ฟ ํค๋์ ๊ฐ์ผ๋ฏ๋ก, ๊ตฌํ์ ์๋ฃํ์๋ค.
์๋ฒ ์ชฝ์ ์์ง ๊ตฌํ์ ํ์ง ์์์ผ๋ฏ๋ก ๋ก๊ทธ์ธ ๋ฒํผ ๋๋ฌ๋
๋ฐ์์ด ์์ผ๋ฉฐ, ํฐ๋ฏธ๋์ฐฝ์์ ์ค์๊ฐ ๋ฉ์ธ์ง ํ์ธ ๊ฐ๋ฅํ๋ค.
โ๏ธ /userinfo
sessionId
๋ก ์ ์ฅํด ๋์ ๊ฐ์ ์ด์ฉํด์ ํด๋ผ์ด์ธํธ๋ก ๋ณด๋ด์ค ์ ์ ์ ๋ณด๋ฅผ ์ฐพ์ ๋ค์ ์๋ต์ผ๋ก ๋ณด๋ด์ค์๋ค.cookieId
๋ฅผ sessionId
๋ก ๋ฐ๊ฟ์ฃผ๊ฒ ์ต๋๋ค.const sessionId = req.session.sessionId
const userInfo = {
...USER_DATA.filter((user) => user.id === sessionId)[0],
};
if (!sessionId || !userInfo.id){
res.status(401).send('Not Authorized');
} else {
delete userInfo.password
res.send(userInfo)
}
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
userinfo ๋ก์ง์ ๊ตฌํํ์ผ๋ฏ๋ก, ๋ก๊ทธ์ธ ๋ฒํผ ๋๋ฅด๋ฉด ๋ก๊ทธ์ธ ๊ฐ๋ฅํ๋ค.
โ๏ธ /logout
req.session.destroy
๊ฐ ์์ต๋๋ค. ์ด ๋ฉ์๋๋ฅผ ์ฌ์ฉํด ์ฃผ๋ฉด ๋ฉ๋๋ค.req.session.destroy();
res.status(205).send('Logged Out Successfully');
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
ํ ํฐ์ ์ผ์ข ์ธ JSON Web Token์ ์ด์ฉํ์ฌ ํ ํฐ๋ฐฉ์ ์ธ์ฆ์ ๊ตฌํํ๋ค.
ํด๋ผ์ด์ธํธ ์ฝ๋๋ ์ฟ ํค ๋ฐ ์ธ์
์ธ์ฆ๋ฐฉ์๊ณผ ๋์ผํฉ๋๋ค.
๊ธฐ์กด ์ฟ ํค, ์ธ์
๊ณผ์ ์์ ํ์๋ ํด๋ผ์ด์ธํธ ์ฝ๋๋ฅผ ๊ฐ์ ธ์๋ ๋ฉ๋๋ค.
์ด๋ฒ ๊ณผ์ ์์๋ JWT ์์ฑ ๋ฐ ๊ฒ์ฆ์ ๋์์ฃผ๋ ํฌํผ ํ์
์ด ์์ต๋๋ค.
helper/tokenFunctions.js์์ ๊ฐ๊ฐ์ ๋ฉ์๋๋ค์ ์ญํ ์ ๋จผ์ ํ์
ํ์ธ์.
๋ค์ ์์๋ก ์ฝ๋๋ฅผ ๊ตฌํํฉ๋๋ค. ๊ฐ ํ์ผ์๋ ์ ์ ํ ์์น์ TODO:๊ฐ ์กด์ฌํฉ๋๋ค.
1. controllers/login.js (POST /users/login)
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
const { USER_DATA } = require("../../db/data");
const { generateToken } = require("../helper/tokenFunctions");
module.exports = async (req, res) => {
const { userId, password } = req.body.loginInfo;
const { checkedKeepLogin } = req.body;
const userInfo = USER_DATA.filter(
(user) => user.userId === userId && user.password === password
);
if (userInfo.length === 0) {
res.status(401).send("Not Authorized");
} else {
const user = userInfo[0]; // ์ฒซ ๋ฒ์งธ ์ผ์นํ๋ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
const accessToken = generateToken(user, "access");
const refreshToken = generateToken(user, "refresh");
if (checkedKeepLogin) {
res.cookie("access_jwt", accessToken, { httpOnly: true });
res.cookie("refresh_jwt", refreshToken, { httpOnly: true });
} else {
res.cookie("access_jwt", accessToken, { httpOnly: true });
}
res.redirect("/userinfo");
}
};
2. controllers/userInfo.js (GET /users/userinfo)
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
const { USER_DATA } = require("../../db/data");
const { verifyToken, generateToken } = require("../helper/tokenFunctions");
module.exports = async (req, res) => {
const accessToken = req.cookies.access_jwt;
const refreshToken = req.cookies.refresh_jwt;
if (!accessToken && refreshToken) {
try {
// Refresh Token ๊ฒ์ฆ
const decodedRefreshToken = verifyToken("refresh", refreshToken);
const { id } = decodedRefreshToken;
// Access Token ์ฌ๋ฐ๊ธ
const newAccessToken = generateToken({ id }, "access");
res.cookie("access_jwt", newAccessToken, { httpOnly: true });
// ์ ์ ์ ๋ณด ์กฐํ
const userInfo = USER_DATA.find((user) => user.id === id);
if (userInfo) {
res.status(200).json(userInfo);
} else {
res.status(401).send("Not Authorized");
}
} catch (refreshTokenError) {
res.status(401).send("Not Authorized");
}
} else if (accessToken) {
try {
// Access Token ๊ฒ์ฆ
const decodedAccessToken = verifyToken("access", accessToken);
const { id } = decodedAccessToken;
// ์ ์ ์ ๋ณด ์กฐํ
const userInfo = USER_DATA.find((user) => user.id === id);
if (userInfo) {
res.status(200).json(userInfo);
} else {
res.status(401).send("Not Authorized");
}
} catch (accessTokenError) {
if (accessTokenError.name === "TokenExpiredError") {
res.status(401).send("Not Authorized");
} else {
res.status(500).send("Internal Server Error");
}
}
} else {
res.status(401).send("Not Authorized");
}
};
3. controllers/logout.js (POST /users/logout)
๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
module.exports = (req, res) => {
res.clearCookie("access_jwt");
res.clearCookie("refresh_jwt");
res.status(205).send();
}
์ด๋ฒ ๊ณผ์ ์์๋ ์๋ฒ๊ฐ ์๋ ํด๋ผ์ด์ธํธ๋ฅผ ๊ตฌํํด์ผ ํฉ๋๋ค. ๋จผ์ ์๋ฒ์์ ๊ฐ ์ปจํธ๋กค๋ฌ์ ์ญํ ์ ํ์ ํ์ธ์.
Login with Github
๋ฒํผ์ ํด๋ฆญํ์ ๋, Github์์ ์ธ์ฆ์ด ์ฑ๊ณตํ๋ฉด Mypage์์ ๋์ ์ ์ ์ ๋ณด๋ฅผ ํ์ธํ ์ ์์ด์ผ ํฉ๋๋ค.
1. GitHub์ ๋ด ์ฑ ๋ฑ๋ก
๋งํฌ ๋ฅผ ์ฐธ๊ณ ํ์ฌ, OAuth ์ฑ์ ๋ฑ๋กํฉ๋๋ค.
Homepage URL ๋ฐ Authorization callback URL์
ํด๋น ๊ณผ์ ์ํด๋ผ์ด์ธํธ ์ฃผ์(http://localhost:3000
)๋ก ๋ฆฌ๋๋ ์
ํฉ๋๋ค.
2. ํ๊ฒฝ ์ค์
GitHub App์์ ์ ๊ณตํ๋ Client ID ๋ฐ Client Secret์ ์ ๋ณด๋ฅผ ์ฑ์ ๋ฃ์ด์ผ ํฉ๋๋ค.
.env
๋ก ์์ ํ์๊ณ ,ํด๋น ๊ณผ์ ์์ OAuth App์ ํตํด Access token์ ๋ฐ์์ค๋ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
3. ํ๊ฒฝ ์ค์
4. Authorization code ๋ฐ์์ค๊ธฐ (Login Component)
npm start
๋ช
๋ น์ด๋ก ํด๋ผ์ด์ธํธ๋ฅผ ์คํ์์ผ ๋ณด์ธ์!App.js
๋ฅผ ์์์ผ๋ก ์ฃผ์์ ์ ํ ์๋ ๊ฐ์ด๋๋ผ์ธ์ ์ฐธ๊ณ ํด ์์ฑ์ ์์ํ์ธ์.5. Access token ๋ฐ์ ์ค๊ธฐ (App Component)
App.js
์ getAccessToken
ํจ์๊ฐ ์คํ๋ฉ๋๋ค./callback
์๋ํฌ์ธํธ๋ก ์์ฒญ์ ๋ณด๋ด๊ณ ์๋ต์ผ๋ก ๋ฐ์์จ Access token์ App ์ปดํฌ๋ํธ์ state
์ ์ ์ฅํ ํ, Mypage
์ปดํฌ๋ํธ์์ props๋ก ๋ด๋ ค๋ฐ์ ํ์ฉํ์ธ์.6. ๋ก์ปฌ ์๋ฒ๋ฅผ ํตํด Github ๋ฆฌ์์ค ์๋ฒ์ ์ ์ ์ ๋ณด ์์ฒญ (Mypage Component)
๋ฐ์์จ Access Token์ผ๋ก Mypage
์ปดํฌ๋ํธ์์ ๋ฆฌ์์ค์ ๋ํ API ์์ฒญ์ ํ ์ ์์ต๋๋ค. ์ก์ธ์ค ํ ํฐ์ ์ด์ฉํด /userinfo
๋ก ์์ฒญํ๋ฉด ๋ก์ปฌ ์๋ฒ์ ๋ฆฌ์์ค์ธ serverResource์
Github Resource ์๋ฒ์ ์์ฒญํ ๋ฆฌ์์ค์ธ githubUserData
๊ฐ ์๋ต์ผ๋ก ์ ๋ฌ๋ฉ๋๋ค.
์์ฑ๋ ๋ง์ดํ์ด์ง ํ๋ฉด์๋ ๋ด Github ์ ์ ์ ๋ณด์ ๋ก์ปฌ ์๋ฒ์ ์๋ ์ ๋ณด๊ฐ ๋ด๊ฒจ์ผ ํฉ๋๋ค.
7. ๋ก๊ทธ์์ ๊ตฌํํ๊ธฐ (Mypage Component)
/logout
์ ์ก์ธ์ค ํ ํฐ์ ์ด์ฉํ ์์ฒญ์ ๋ณด๋ด ์ ์ ์ ํ ํฐ์ ์ง์์ผ ํฉ๋๋ค. ์์ฒญ์ด ์ฑ๊ณตํ๋ฉด ๊ทธ์ ๋ฐ๋ฅธ ์ํ ๋ํ ์
๋ฐ์ดํธํด์ผ ํฉ๋๋ค.๊ตฌํ ์ฝ๋ & ๊ฒฐ๊ณผ ํ๋ฉด
// Client - Userinfo.js
import React from 'react';
export default function User({ githubUser, serverResource, logoutHandler }) {
return (
<>
<img src={githubUser.avatar_url} alt='github_avatar' />
<h3>๋ด ์ ๋ณด</h3>
<div className='userinfo-field'>
<div>
{`๐ Studying at `} <b>{serverResource.bootcamp}</b>
</div>
<div>{`๐ Living in ${githubUser.location}`}</div>
<div>{`๐ฌ Contact: ${githubUser.email}`}</div>
<div>{`๐ฉ๐ปโ๐ป ${serverResource.position}`}</div>
<article>
<h3>Bio</h3>
<span>{githubUser.bio ? githubUser.bio : serverResource.bio}</span>
</article>
</div>
<button id='logout-btn' onClick={logoutHandler}>
LOGOUT
</button>
</>
);
}
์ ๋ง ์ ๊ธฐํ๊ฒ ํด๋ผ์ด์ธํธ Userinfo ์ฝ๋์์ ์ค์ ํ ๊ฐ
๊ทธ๋๋ก ํ๋กํ ์ฌ์ง์ด๋ ๋ด๋ถ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์จ๋ค..
๋งค์ผ ์ด์ฉ๋ง ํ๋ OAuth๋ฅผ ์ง์ ๊ตฌํํ๋๊น ์ ๋ง ์ ๊ธฐํ๋ค.. ์ ์ธ๊ณ..
๋ค์์ 5, 6 ,7๋ฒ์ ๊ตฌํํ ์ฝ๋๋ฅผ ์ฒจ๋ถํ๊ฒ ๋ค.
// Client - App.js
import "./App.css";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Login from "./pages/Login";
import Mypage from "./pages/Mypage";
import { useEffect, useState } from "react";
import axios from "axios";
function App() {
const [isLogin, setIsLogin] = useState(false);
const [accessToken, setAccessToken] = useState("");
const getAccessToken = async (authorizationCode) => {
// ๋ฐ์์จ Authorization Code๋ก ๋ค์ OAuth App์ ์์ฒญํด์ Access Token์ ๋ฐ์ ์ ์์ต๋๋ค.
// Access Token์ ๋ณด์ ์ ์ง๊ฐ ํ์ํ๊ธฐ ๋๋ฌธ์ ํด๋ผ์ด์ธํธ์์ ์ง์ OAuth App์ ์์ฒญ์ ํ๋ ๋ฐฉ๋ฒ์ ๋ณด์์ ์ทจ์ฝํ ์ ์์ต๋๋ค.
// Authorization Code๋ฅผ ์๋ฒ๋ก ๋ณด๋ด์ฃผ๊ณ ์๋ฒ์์ Access Token ์์ฒญ์ ํ๋ ๊ฒ์ด ์ ์ ํฉ๋๋ค.
// TODO: ์๋ฒ์ /callback ์๋ํฌ์ธํธ๋ก Authorization Code๋ฅผ ๋ณด๋ด์ฃผ๊ณ Access Token์ ๋ฐ์์ต๋๋ค.
// Access Token์ ๋ฐ์์จ ํ state์ Access Token์ ์ ์ฅํ์ธ์
axios
.post("http://localhost:4000/callback", { authorizationCode })
.then((res) => {
setIsLogin(true);
setAccessToken(res.data.accessToken);
console.log(res.data);
});
};
useEffect(() => {
// Authorization Server๋ก๋ถํฐ ํด๋ผ์ด์ธํธ๋ก ๋ฆฌ๋๋ ์
๋ ๊ฒฝ์ฐ, Authorization Code๊ฐ ํจ๊ป ์ ๋ฌ๋ฉ๋๋ค.
// ex) http://localhost:3000/mypage?code=5e52fb85d6a1ed46a51f
const url = new URL(window.location.href);
const authorizationCode = url.searchParams.get("code");
if (authorizationCode) {
getAccessToken(authorizationCode);
}
}, []);
return (
<BrowserRouter>
<div className="main">
<div className="container">
<Routes>
<Route
path="/"
element={
isLogin ? (
<Mypage
/*TODO: ์ปดํฌ๋ํธ์ ํ์ํ props๋ฅผ ์ ๋ฌํ์ธ์. */
accessToken={accessToken}
setIsLogin={setIsLogin}
/>
) : (
<Login />
)
}
/>
</Routes>
</div>
</div>
</BrowserRouter>
);
}
export default App;
// Client - Login.js
import React from "react";
import githubLogo from "./../images/github.png";
export default function Login() {
const CLIENT_ID = process.env.REACT_APP_CLIENT_ID;
const loginRequestHandler = () => {
// TODO: GitHub๋ก๋ถํฐ ์ฌ์ฉ์ ์ธ์ฆ์ ์ํด GitHub๋ก ์ด๋ํด์ผ ํฉ๋๋ค. ์ ์ ํ URL์ ์
๋ ฅํ์ธ์.
// OAuth ์ธ์ฆ์ด ์๋ฃ๋๋ฉด authorization code์ ํจ๊ป callback url๋ก ๋ฆฌ๋๋ ์
ํฉ๋๋ค.
// ์ฐธ๊ณ : https://docs.github.com/en/free-pro-team@latest/developers/apps/identifying-and-authorizing-users-for-github-apps
return window.location.assign(
`https://github.com/login/oauth/authorize?client_id=${CLIENT_ID}`
);
};
return (
<>
<div className="left-box">
<span>
Education
<p>for the</p>
Real World
</span>
</div>
<div className="right-box">
<div className="right-box">
<h1>AUTH STATES</h1>
<h3>OAuth 2.0 ์์
๋ก๊ทธ์ธ</h3>
<form onSubmit={(e) => e.preventDefault()}>
<div className="input-field">
<button type="submit" onClick={loginRequestHandler}>
<img id="logo" alt="logo" src={githubLogo} />
<span> LOGIN WITH GITHUB</span>
</button>
</div>
</form>
</div>
</div>
</>
);
}
// Client - Mypage.js
import axios from "axios";
import React, { useEffect, useState } from "react";
import Loading from "./components/Loading";
import User from "./components/UserInfo";
export default function Mypage({ accessToken, setIsLogin }) {
const [githubUser, setGithubUser] = useState(null);
const [serverResource, setServerResource] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const logoutHandler = () => {
// TODO: /logout์ ํตํด ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์์๋๋๋ก ๊ตฌํํ์ธ์.
// prop์ผ๋ก ๋ฐ์ Access Token์ ์ด์ฉํด /logout ์๋ํฌ์ธํธ๋ก ์์ฒญ์ ๋ณด๋ด์ผํฉ๋๋ค.
// ์์ฒญ์ด ์ฑ๊ณตํ๋ค๋ฉด isLogin ์ํ๋ฅผ false๋ก ์
๋ฐ์ดํธํด์ผ ํฉ๋๋ค.
axios
.delete("http://localhost:4000/logout", {
data: {
accessToken,
},
})
.then(() => setIsLogin(false))
.catch((err) => console.log("๋ค์ ์๋ํ์ญ์์ค."));
};
useEffect(() => {
// TODO: /userinfo๋ฅผ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์์ค์ธ์.
// prop์ผ๋ก ๋ฐ์ Access Token์ ์ด์ฉํด /userinfo ์๋ํฌ์ธํธ๋ก ์์ฒญ์ ๋ณด๋ด์ผํฉ๋๋ค.
// ์๋ต์ผ๋ก ๋ฐ์ ๋ฐ์ดํฐ๋ฅผ githubUser, serverResource์ ์ํ๋ก ์
๋ฐ์ดํธํด์ผํฉ๋๋ค.
// isLoading ์ํ๋ฅผ false๋ก ์
๋ฐ์ดํธํด์ผ ํฉ๋๋ค.
if (!accessToken) {
return; // accessToken์ด ์์ผ๋ฉด ์์ฒญ์ ๋ณด๋ด์ง ์์
}
axios
.post("http://localhost:4000/userinfo", { accessToken })
.then((res) => {
setGithubUser(res.data.githubUserData);
setServerResource(res.data.serverResource);
setIsLoading(false);
})
.catch((err) => console.log("๋ค์ ์๋ํ์ญ์์ค."));
}, [accessToken]);
return (
<>
<div className="left-box">
{!isLoading && (
<span>
{`${githubUser.login}`}๋,
<p>๋ฐ๊ฐ์ต๋๋ค!</p>
</span>
)}
</div>
<div className="right-box">
<div className="input-field">
{isLoading ? (
<Loading />
) : (
<User
githubUser={githubUser}
serverResource={serverResource}
logoutHandler={logoutHandler}
/>
)}
</div>
</div>
</>
);
}
๐ก Delete, Payload, body ?
Client ํด๋์์ Mypage.js ํ์ผ์ ๊ตฌํํ ๋, 'delete ์์ฒญ์์ accessToken ๊ฐ์ ์ .then์ผ๋ก ์ฒ๋ฆฌํ์ง ์๊ณ ํ์ด๋ก๋๋ก ์ฒ๋ฆฌํ ๊น' ์ 'accessToken ๊ฐ์ ์ data ์์ ๋ด์์ ์ ๋ฌํ ๊น'ํ๋ ์๋ฌธ์์ ์์๋์๋ค.
ํ์ด๋ก๋(Payload)์ .then()
๋ฉ์๋๋ HTTP ์์ฒญ์ ๋ค๋ฅธ ์ธก๋ฉด์์ ๋ค๋ฃจ๋ ๊ฐ๋
์ด๋ค.
๊ฐ๊ฐ์ ์ญํ ๊ณผ ์ฐจ์ด์ , ์ฌ์ฉ๋ฒ์ ์ดํด๋ณด๊ฒ ๋ค.
data
์์ฑ์ ์ฌ์ฉํ๋ค.axios.post(url, data)
.then()
๋ฉ์๋:.then()
๋ฉ์๋๋ Promise ๊ฐ์ฒด์ ๋ฉ์๋ ์ค ํ๋์ด๋ค..then()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์๋ต ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ๊ฑฐ๋ ํน์ ์์
์ ์ํํ ์ ์๋ค..then()
๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ๋น๋๊ธฐ ์์ฒญ์ ์ฑ๊ณต ์ฝ๋ฐฑ์ ์ ์ํ๋ค.axios.get(url).then(response => { // ์ฑ๊ณต ์ฒ๋ฆฌ })
์์ฝํ๋ฉด ํ์ด๋ก๋๋ ์์ฒญ ๋ณธ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ธฐ ์ํด ์ฌ์ฉ๋๊ณ ,
.then()
๋ฉ์๋๋ ๋น๋๊ธฐ ์์ฒญ์ด ์ฑ๊ณต์ ์ผ๋ก ์๋ฃ๋ ํ์ ์คํ๋๋ ์ฝ๋ฐฑ ํจ์๋ฅผ ์ ์ํ๋ค.
๋ฐ๋ผ์ accessToken์ ์ฒ์๋ถํฐ ๋๊ฒจ์ค์ผ ํ๊ธฐ ๋๋ฌธ์ payload(์์น)์ ์๋ ๊ฒ์ด ๋ง๋ค.
๊ทธ๋ฆฌ๊ณ ์ด ๋งํฌ์ ๋ฐ๋ฅด๋ฉด HTTP DELETE ๋ฉ์๋์๋ ์์ฒญ ๋ณธ๋ฌธ์ด ์์ด์ผ ํ๋ค.
HTTP ํ๋กํ ์ฝ์ ๊ธฐ๋ณธ์ ์ผ๋ก DELETE ์์ฒญ์์ ๋ณธ๋ฌธ(payload)์ ์ ์กํ๊ธฐ ์ํ ๋ช ์์ ์ธ ๊ท์ฝ์ ์ ๊ณตํ์ง ์๋๋ค.
HTTP ํ์ค์ ๋ฐ๋ฅด๋ฉด DELETE ์์ฒญ์ ์ฃผ๋ก ๋ฆฌ์์ค ์ญ์ ๋ฅผ ์ํด ์ฌ์ฉ๋๋ ๋ฉ์๋์ด๋ค.
๋ฐ๋ผ์ DELETE ์์ฒญ์์ ๋ณธ๋ฌธ์ ์ฌ์ฉํ๋ ๊ฒ์ ์ผ๋ฐ์ ์ผ๋ก ์์ธ์ ์ธ ์ํฉ์์ ์ฌ์ฉ๋๋ค.
data ์์ฑ์ ์ฌ์ฉํ๋ ์ด์ ๋ ๋ค์๊ณผ ๊ฐ๋ค:
Axios๊ฐ DELETE ์์ฒญ์ ๋ณด๋ผ ๋, ๊ธฐ๋ณธ์ ์ผ๋ก ๋ณธ๋ฌธ์ด ๋น์ด์๋ค๊ณ ๊ฐ์ ํ๋ค. ๊ทธ๋ฌ๋ ๋ช๋ช API ์๋ฒ๋ DELETE ์์ฒญ์ ๋ณธ๋ฌธ์ ์ฌ์ฉํ๊ธฐ๋ฅผ ์ํ ์ ์๋ค. data ์์ฑ์ ์ฌ์ฉํ๋ฉด Axios๊ฐ DELETE ์์ฒญ์ ๋ณธ๋ฌธ์ ํฌํจ์ํฌ ์ ์๊ฒ ๋๋ค.
โจ DELETE ์์ฒญ์ ๋ณธ๋ฌธ์ ์๋ฒ์์ ํน์ ๋ฐ์ดํฐ๋ฅผ ๊ธฐ๋ํ๊ฑฐ๋ ์ฒ๋ฆฌํ ๋ ์ ์ฉํ ์ ์๋ค. ์๋ฅผ ๋ค์ด, ์ก์ธ์ค ํ ํฐ์ DELETE ์์ฒญ์ ๋ณธ๋ฌธ์ ํฌํจํ์ฌ ๋ก๊ทธ์์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๊ฒฝ์ฐ, ์๋ฒ๋ ์ก์ธ์ค ํ ํฐ์ ํ์ธํ๊ณ ํด๋น ์ฌ์ฉ์๋ฅผ ๋ก๊ทธ์์ํ ์ ์๋ค.
์ผ๋ถ ์๋ฒ ๊ตฌํ์ DELETE ์์ฒญ์ ๋ณธ๋ฌธ์ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ค ํธ๋ฆฌํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋๋ก ์ค๊ณ๋์ด ์์ ์ ์๋ค. ์ด๋ฌํ ๊ฒฝ์ฐ, data ์์ฑ์ ์ฌ์ฉํ์ฌ DELETE ์์ฒญ์ ๋ณธ๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ์ ๋ฌํ๋ ๊ฒ์ด ๋ฐ๋์งํ ๋ฐฉ๋ฒ์ด๋ค.
๊ทธ๋ฌ๋ DELETE ์์ฒญ์์ ๋ฐ์ดํฐ๋ฅผ ๋ณธ๋ฌธ์ ํฌํจํ๋ ๊ฒ์
์ผ๋ฐ์ ์ธ ํจํด์ ์๋๋ฉฐ, ๋ชจ๋ ์๋ฒ๊ฐ ์ด๋ฅผ ์ง์ํ๋ ๊ฒ์ ์๋๋ค.
API ๋ฌธ์๋ฅผ ํ์ธํ๊ฑฐ๋ ์๋ฒ ๊ฐ๋ฐ์์ ํ์ํ์ฌ ์ฌ๋ฐ๋ฅธ ์์ฒญ ๋ฐฉ์์ ๊ฒฐ์ ํ๋ ๊ฒ์ด ์ข๋ค.
axios
.post("http://localhost:4000/userinfo", { accessToken })
.then((res) => {
setGithubUser(res.data.githubUserData);
setServerResource(res.data.serverResource);
setIsLoading(false);
})
.catch((err) => console.log("๋ค์ ์๋ํ์ญ์์ค."));
}, [accessToken]);
axios
.delete("http://localhost:4000/logout", {
data: {
accessToken,
},
})
.then(() => setIsLogin(false))
.catch((err) => console.log("๋ค์ ์๋ํ์ญ์์ค."));
์ค๋ ๊ตฌํํ ์์ ์ฝ๋๋ฅผ ๋ด๋ post๋ ๊ทธ๋ฅ ์ ๋ฌํ๋, delete๋ data์ ๋ด์์ ์ ๋ฌํ๋ค.
๋ฐ๋ผ์ delete ๋ฉ์๋๋ payload body ๊ฐ ํ์๊ฐ ์๊ธฐ ๋๋ฌธ์,
Delete ๋ฉ์๋๋ก payload body๋ฅผ ์ค์ด ์์ฒญ์ ํ๋ฉด ์์ฒญ์ด ๊ฑฐ์ ๋ ์ ์๋ค.
์ ๋ฌ์ด ํ์ํ ์ํฉ์ด๋ผ๋ฉด, data ์์ฑ์ ๋ด์์ ์์ฒญํด์ผ ํ๋ค.