https://www.youtube.com/watch?v=VePjN663uwc
프로젝트 초기 세팅
- 먼저 React 클라이언트를 생성
npx create-react-app@latest jwttest
서버 세팅
1. 서버 폴더 생성 및 패키지 설치
# 1. server 폴더 생성
mkdir server
# 2. 생성한 폴더로 이동
cd server
# 3. npm 초기화 (package.json 생성)
npm init -y
# 4. 필요한 패키지 설치
npm i express nodemon dotenv cors jsonwebtoken cookie-parser
각 패키지의 역할
- express : Node.js 웹 서버 프레임워크
- nodemon : 파일 변경 시 서버를 자동으로 재시작해주는 개발용 도구
- dotenv : .env 파일에서 환경변수를 불러오는 라이브러리
- cors : 클라이언트(React, 포트 3000)와 서버(포트 8123)가 다른 출처이기 때문에 교차 출처 요청을 허용하기 위해 필요
- jsonwebtoken : JWT 토큰 생성 및 검증 라이브러리
- cookie-parser : 요청에 담긴 쿠키를 파싱해서 req.cookies로 사용할 수 있게 해주는 미들웨어
2. server/package.json 확인 및 스크립트 수정
- 설치가 완료되면 server/package.json의 dependencies가 아래와 같이 구성된다.
"dependencies": {
"cookie-parser": "^1.4.7",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"jsonwebtoken": "^9.0.3",
"nodemon": "^3.1.11"
}
- 추가로 시작 명령어를 수정해준다.
- dev 스크립트를 통해 개발 중에는 nodemon으로 서버를 실행하면 코드를 수정할 때마다 서버를 수동으로 재시작하지 않아도 된다.
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
3. server/.env 환경 변수 설정
- .env 파일은 포트번호, JWT 시크릿 키처럼 외부에 노출되면 안 되는 값들을 관리하는 파일이다. 추후 .gitignore에 반드시 추가해서 깃허브에 올라가지 않도록 주의해야 한다.
PORT=8123
4. server/index.js 서버 진입점 작성
- cors 설정에서 credentials: true를 반드시 설정해야 클라이언트에서 쿠키를 포함한 요청을 보낼 수 있다.
- React 쪽에서도 요청 시 withCredentials: true 옵션을 함께 설정해줘야 한다.
const express = require('express');
const dotenv = require('dotenv');
const cookieParser = require('cookie-parser');
const cors = require('cors');
const app = express();
dotenv.config();
const port = process.env.PORT || 3002;
app.use(cors({ // 교차 출처 허용
origin: 'http://localhost:3000',
methods: ['GET', 'POST'],
credentials: true
}));
app.use(express.json()); // JSON 데이터 읽기용
app.use(cookieParser()); // 쿠키 읽기용
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
server 폴더 안에서 실행 >> npm run dev
5. server/Database.js 테스트용 데이터베이스 생성
- 간단하게 배열로 유저 데이터를 만들어 사용한다.
module.exports = [
{ id: 1, username: 'testuser1', email: 'testuser1@test.com', password: '1111' },
{ id: 2, username: 'testuser2', email: 'testuser2@test.com', password: '2222' },
{ id: 3, username: 'testuser3', email: 'testuser3@test.com', password: '3333' },
{ id: 4, username: 'testuser4', email: 'testuser4@test.com', password: '4444' },
]
실제 서비스에서는 비밀번호를 평문으로 저장하면 절대 안 된다.
bcrypt 같은 라이브러리로 해싱해서 저장하는 것이 원칙이다.
6. server/controller/index.js 컨트롤러 분리
- 로직을 index.js에 전부 작성하면 코드가 복잡해지기 때문에, controller 폴더를 따로 생성해서 라우트별 핸들러 함수를 분리해 관리한다.
- 이런 구조를 관심사의 분리(Separation of Concerns) 라고 하며, 코드 유지보수가 훨씬 편해진다.
const userDatabase = require('../Database');
const jwt = require('jsonwebtoken');
const login = (req, res) => {}
const accessToken = (req, res) => {}
const refreshToken = (req, res) => {}
const loginSuccess = (req, res) => {}
const logout = (req, res) => {}
module.exports = { login, accessToken, refreshToken, loginSuccess, logout }
함수역할
| login | 이메일/비밀번호 검증 후 Access Token, Refresh Token 발급 |
| accessToken | Access Token의 유효성을 검증 |
| refreshToken | Refresh Token을 이용해 새로운 Access Token 재발급 |
| loginSuccess | 로그인 성공 여부 확인 및 유저 정보 반환 |
| logout | 쿠키에서 토큰 삭제 처리 |
7. server/index.js 라우트 연결
- server/index.js에 컨트롤러를 불러와서 각 경로에 연결
const {login, accessToken, refreshToken, loginSuccess, logout} = require('./controller');
app.post('/login', login)
app.get('/accesstoken', accessToken)
app.get('/refreshtoken', refreshToken)
app.get('/login/success', loginSuccess)
app.post('/logout', logout)
라우트 구성을 정리
| POST | /login | 로그인 요청 |
| GET | /accesstoken | Access Token 검증 |
| GET | /refreshtoken | Refresh Token으로 토큰 재발급 |
| GET | /login/success | 로그인 상태 확인 |
| POST | /logout | 로그아웃 |
https://www.youtube.com/watch?v=NIKYWadDAbo
npm i axios
npm install --save-dev @types/react @types/react-dom
로그인 & JWT 토큰 발급 흐름 정리
- 전체 흐름 : 사용자가 이메일/비밀번호를 입력 → 서버에서 검증 → Access Token + Refresh Token 발급 → 쿠키에 담아 응답
server/.env
- .env 값 앞뒤 공백 주의
PORT=8123
ACCESS_SECRET=accesssecret
REFRESH_SECRET=refreshsecret
server/controller/index.js
- 분리해뒀던 함수 중 login(이메일/비밀번호 검증 후 Access Token, Refresh Token 발급) 함수를 컨트롤 해준다.
1. 요청에서 사용자 정보 추출
- 클라이언트가 POST 요청으로 보낸 body에서 이메일과 비밀번호를 꺼낸다.
const {email, password} = req.body;
2. 사용자 조회
⚠️ 강의에서 filter()를 사용했는데 filter()는 조건에 맞는 요소를 배열로 반환하기 때문에, userInfo는 항상 배열이다.
빈 배열 []도 JavaScript에서는 truthy로 평가되기 때문에, 사용자가 없어도 if(!userInfo)가 절대 실행되지 않는다.
올바르게 수정하려면 find()를 사용하는 게 적합하다.
// const userInfo = userDatabase.filter(item => {
// return item.email === email
// })
const userInfo = userDatabase.find(item => item.email === email);
3. Access Token 발급
구조 : jwt.sign(payload, secretKey, options)
- payload: 토큰 안에 담길 사용자 정보 (민감한 정보는 절대 넣지마)
- secretKey: .env에서 불러온 비밀 키
- expiresIn '1m': 1분 후 만료 → Access Token은 짧게 설정하는 것이 보안상 중요
const accessToken = jwt.sign(
{ id: userInfo.id, username: userInfo.username, email: userInfo.email },
process.env.ACCESS_SECRET,
{ expiresIn: '1m', issuer: 'About Tech' }
);
4. Refresh Token 발급
- Access Token이 만료됐을 때 새로 발급받기 위한 토큰이다.
- 유효기간이 길기 때문에 서버 DB에 저장해두고 검증하는 것이 일반적이다.
const refreshToken = jwt.sign(
{ id: userInfo.id, username: userInfo.username, email: userInfo.email},
process.env.REFRESH_SECRET,
{ expiresIn: '24h', issuer: 'About Tech', }
);
5. 쿠키에 토큰 저장
- httpOnly: true → JavaScript에서 쿠키 접근 불가 → XSS 공격 방어
- secure: false → HTTPS가 아닌 환경(로컬)에서도 동작하도록 설정. 실제 배포 시엔 반드시 true로 변경해야 한다
res.cookie('accessToken', accessToken, { secure: false, httpOnly: true })
res.cookie('refreshToken', refreshToken, { secure: false, httpOnly: true })
6. 완성코드
const login = (req, res, next) => {
// 1. 요청에서 사용자 정보 추출
const {email, password} = req.body;
// 2. 사용자 조회
const userInfo = userDatabase.find(item => item.email === email);
if(!userInfo){
res.status(403).json("Not Authorized")
}else{
try{
// access Token 발급
const accessToken = jwt.sign({
id: userInfo.id,
username: userInfo.username,
email: userInfo.email,
}, process.env.ACCESS_SECRET,{
expiresIn: '1m',
issuer: 'About Tech',
})
// refresh Token 발급
const refreshToken = jwt.sign({
id: userInfo.id,
username: userInfo.username,
email: userInfo.email,
}, process.env.REFRESH_SECRET,{
expiresIn: '24h',
issuer: 'About Tech',
})
// token 전송
res.cookie('accessToken', accessToken, {
secure: false,
httpOnly: true,
})
res.cookie('refreshToken', refreshToken, {
secure: false,
httpOnly: true,
})
res.status(200).json("Login Success")
}catch(errer){
res.status(500).json("Server Error")
}
}
}

현재 비밀번호 검증은 생략된 상태다. 실제 서비스에서는 bcrypt로 해시된 비밀번호를 비교해야 한다.
const isPasswordValid = await bcrypt.compare(password, userInfo.password);
https://www.youtube.com/watch?v=rh8Ptrw8LpU
JSON Web Tokens - jwt.io
JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).
www.jwt.io


Access Token으로 사용자 인증
로그인 시 발급받은 accessToken이 브라우저 쿠키에 저장되어 있다면,
이를 다시 서버로 보내 "현재 로그인한 사용자가 누구인지" 특정할 수 있다.
1. Client: 서버에 토큰 인증 요청 (App.js)
- 먼저 클라이언트에서 서버의 /accessToken 엔드포인트로 GET 요청을 보낸다.
- 이때 중요한 점 : withCredentials: true 옵션이 없으면 브라우저가 쿠키를 요청에 포함시키지 않아 서버에서 토큰을 읽을 수 없다.
const accessToken = () => {
axios({
method: 'GET',
url: 'http://localhost:8123/accessToken',
withCredentials: true, // 쿠키 전송을 위한 필수 설정
})
}
// ... JSX 부분
<a onClick={accessToken} className='App-link'>
get Access Token
</a>
2. Server: 라우터 설정 (index.js)
- 서버에서는 해당 경로로 들어오는 요청을 컨트롤러 함수와 연결
app.get('/accesstoken', accessToken)
3. Server: 컨트롤러 로직 작성 (controller/index.js)
- 쿠키에 담긴 토큰을 꺼내 검증하고, 데이터베이스에서 사용자 정보를 조회하는 핵심 로직이다.
- req.cookies로 httpOnly 쿠키에 저장된 토큰을 꺼내고, jwt.verify()로 위변조 여부와 만료 여부를 동시에 검증한다.
- 검증 성공 시 payload에서 꺼낸 data.id로 DB를 한 번 더 조회하는데,
- 이는 토큰 발급 이후 탈퇴하거나 권한이 변경된 사용자를 걸러낼 수 있어 안전하다.
const jwt = require('jsonwebtoken');
const accessToken = (req, res) => {
try {
// 1. 쿠키에서 accessToken 읽기
const token = req.cookies.accessToken;
// 2. JWT를 이용해 토큰 검증 및 디코딩
const data = jwt.verify(token, process.env.ACCESS_SECRET);
// 3. 토큰 내부의 id를 이용해 유저 정보 조회 (userDatabase는 임시 DB)
const userData = userDatabase.find(item => item.id === data.id);
if (!userData) {
return res.status(404).json("사용자를 찾을 수 없습니다.");
}
// 4. 보안을 위해 비밀번호 제거 후 응답
const { password, ...others } = userData;
res.status(200).json(others);
} catch (error) {
// 토큰 만료나 조작 시 에러 발생
res.status(500).json(error);
}
}
💡 주요 포인트 요약
- req.cookies: req.cookies를 사용하려면 server/index.js에 app.use(cookieParser())가 등록되어 있어야 한다.
- jwt.verify(): 클라이언트가 보낸 토큰이 서버가 발행한 것이 맞는지, 유효기간이 지나지 않았는지 체크한다.
- 보안(Security): 사용자 정보를 반환할 때 password와 같은 민감한 정보는 반드시 **Destructuring(구조 분해 할당)**을 통해 제외하고 보내야 한다.
토큰을 다 지우고 새로 로그인 후 get Access Token 버튼을 클릭하면 사용자 정보를 응답받게 된다.

response {"id":3,"username":"testuser3","email":"testuser3@test.com"}
같은 방법으로 refreshToken 도 작성해본다. 사용자 정보에 접근할 수 있는 accessToken의 갱신 용도다.
클라이언트에서 refreshToken을 실행하면 access token을 발급받아 사용자정보를 알 수 있게 한다.
1. Client: 서버에 토큰 refresh 요청 (App.js)
- 먼저 클라이언트에서 서버의 /refreshToken 엔드포인트로 GET 요청을 보낸다.
const refreshToken = () => {
axios({
method: 'GET',
url: 'http://localhost:8123/refreshToken',
withCredentials: true,
})
}
// ... JSX 부분
<a onClick={refreshToken} className='App-link'>
get Refresh Token
</a>
2. Server: 라우터 설정 (index.js)
- 서버에서는 해당 경로로 들어오는 요청을 컨트롤러 함수와 연결
app.get('/refreshtoken', refreshToken)
3. Server: 컨트롤러 로직 작성 (controller/index.js)
const refreshToken = (req, res) => {
try{
const token = req.cookies.refreshToken; // 쿠키에서 refreshToken 읽기
const data = jwt.verify(token, process.env.REFRESH_SECRET); // refreshToken 검증
const userData = userDatabase.find(item => item.id === data.id); // userDatabase에서 사용자 정보 찾기
// access Token 발급
const accessToken = jwt.sign({
id: userData.id,
username: userData.username,
email: userData.email,
}, process.env.ACCESS_SECRET,{
expiresIn: '1m',
issuer: 'About Tech',
})
// token 전송
res.cookie('accessToken', accessToken, {
secure: false,
httpOnly: true,
})
res.status(200).json("Access Token Reissued")
}catch(error){
res.status(500).json('Token Error');
}
}
로그인 상태 확인 & 로그아웃
loginSuccess (controller/index.js)
- 앱이 처음 로드될 때 쿠키의 Access Token을 검증해서 로그인 상태인지 확인한다.
const loginSuccess = (req, res) => {
try{
const token = req.cookies.accessToken; // 쿠키에서 accessToken 읽기
const data = jwt.verify(token, process.env.ACCESS_SECRET); // accessToken 검증
const userData = userDatabase.find(item => item.id === data.id); // userDatabase에서 사용자 정보 찾기
res.status(200).json(userData); // 사용자 정보 응답
}catch(error){
res.status(500).json('Token Error');
}
}
logout (controller/index.js)
const logout = (req, res) => {
try {
res.cookie('accessToken', '');
res.status(200).json("Logout Success");
} catch (error) {
res.status(500).json('Token Error');
}
}
App.js에서 로그인 상태 관리
const [isLogin, setIsLogin] = useState(false);
const [user, setUser] = useState({});
useEffect(() => {
try{
axios({
method: 'GET',
url: 'http://localhost:8123/login/success',
withCredentials: true,
})
.then(result => {
if(result.data){
setIsLogin(true);
setUser(result.data);
}
})
.catch((error) => {
console.log(error);
})
}catch(error){
console.log(error);
}
}, [])
// ... JSX 부분
{isLogin ? (
<>
<h3>{user.username} 님이 로그인했습니다.</h3>
<button
onClick={logout}
className='loginButton'
>로그아웃</button>
</>
) :(
<Login setUser={setUser} setIsLogin={setIsLogin} />
)}
로그인 컴포넌트 (src/components/Login.jsx)
const login = () => {
axios({
url: "http://localhost:8123/login",
method: "POST",
withCredentials: true,
data: {
email: email,
password: password,
},
}).then((result) => {
if (result.status === 200) {
window.open('/', '_self')
}
});
};
// ... JSX 부분
<button onClick={login} className="loginButton">Login</button>


전체 흐름 요약
- 로그인 → Access Token(1분) + Refresh Token(24h) 발급, httpOnly 쿠키에 저장
- 페이지 로드 시 /login/success로 Access Token 검증 → 로그인 상태 유지
- Access Token 만료 시 /refreshtoken으로 Refresh Token을 이용해 재발급
- 로그아웃 시 쿠키의 Access Token을 비워서 인증 해제
💡 로그아웃 이후에는 Refresh Token으로 Access Token을 갱신한 뒤 다시 로그인할 수 있다.
'Clone Coding' 카테고리의 다른 글
| AI 코딩 시대, 도구보다 중요한 건 '기본기' (Firebase 활용법) (0) | 2026.02.02 |
|---|---|
| AI 코딩 시대, 도구보다 중요한 건 '기본기' (Cursor AI 활용법) (0) | 2026.02.01 |
| *잠시 중단* N8N & Zapier - Editor State (0) | 2026.01.26 |
| N8N & Zapier - Node selector (0) | 2026.01.22 |
| N8N & Zapier - Editor Setup (1) | 2026.01.20 |