현재 오즈코딩스쿨 강의를 통해 프론트엔드를 학습하고 있습니다.
본 포스트는 해당 강의에 대한 내용 정리를 목적으로 합니다.

JWT (JSON Web Token)는 로그인한 사용자를 인증하기 위해 서버가 발급하는 디지털 신분증이다.
JWT 공식사이트





사용자가 ID와 비밀번호로 로그인하면, 서버는 확인 후 JWT를 발급한다.
클라이언트는 받은 토큰을 로컬스토리지나 쿠키 등에 저장한다.
이후 API 요청 시, 토큰을 Authorization 헤더에 담아 보낸다.
서버는 토큰을 해석하여 사용자를 식별하고, 유효하면 요청을 처리한다.
서버가 로그인 상태를 따로 저장하지 않아도 되므로 무상태(stateless) 인증이 가능하다.
토큰만으로 사용자 확인이 가능해 빠르다.
여러 시스템에서 공통으로 사용할 수 있어 SSO(싱글사인온) 구현에 유리하다.



인증 토큰: 사용자의 신원을 확인하고 세션을 유지하기 위해 사용하는 토큰이다.
예: JWT(JSON Web Token), OAuth 토큰 등이 있다.
접근 토큰(access token): 특정 자원에 대한 접근 권한을 부여받았음을 나타내는 토큰이다.
일반적으로 API 요청 시 헤더에 포함되어 사용된다.
리프레시 토큰(refresh token): 접근 토큰이 만료되었을 때, 새로운 접근 토큰을 발급받기 위해 사용하는 토큰이다.
보안 상 서버에만 저장하는 것이 일반적이다.
CSRF 토큰: 크로스 사이트 요청 위조(CSRF)를 방지하기 위해 사용하는 임시 토큰이다.
주로 폼 전송 시 함께 전송되어 위조 요청을 구분하는 데 사용된다.
세션 토큰: 사용자의 로그인 세션을 식별하기 위해 서버에서 생성하는 토큰이다.
쿠키에 저장되어 클라이언트와 서버 간의 상태를 유지하는 데 사용된다.
API 토큰: 특정 API를 사용할 수 있는 권한을 인증하기 위해 발급되는 토큰이다.
사용량 제한(rate limit)이나 권한 범위(scope)를 설정할 수 있다.
임베디드 토큰: URL 파라미터 등에 포함되어 인증 또는 식별 목적으로 전달되는 토큰이다.
보안에 취약하므로 사용 시 주의가 필요하다.



npm i jsonwebtoken
const express = require('express') // Express 프레임워크를 가져옵니다. 웹 서버를 쉽게 구축할 수 있게 도와줍니다.
const cors = require('cors') // CORS 미들웨어를 가져옵니다. 다른 도메인에서의 요청을 허용/제한하는 데 사용됩니다.
const cookieParser = require('cookie-parser') // cookie-parser 미들웨어를 가져옵니다. (이 코드에서는 사용되지 않지만 포함되어 있습니다.)
const jwt = require('jsonwebtoken') // jsonwebtoken 라이브러리를 가져옵니다. JWT(JSON Web Token)를 생성하고 검증하는 데 사용됩니다.
// 임시 사용자 데이터 배열입니다. 실제 애플리케이션에서는 데이터베이스를 사용해야 합니다.
const users = [
{
user_id: 'test',
user_password: '1234',
user_name: '테스트 유저',
user_info: '테스트 유저입니다'
}
]
const app = express() // Express 애플리케이션 인스턴스를 생성합니다.
// CORS (Cross-Origin Resource Sharing) 설정입니다.
// 클라이언트가 다른 도메인에서 서버로 요청을 보낼 수 있도록 허용합니다.
app.use(cors({
// `origin`: 요청을 보낼 수 있는 클라이언트의 도메인을 설정합니다.
// 개발 환경에서는 'http://127.0.0.1:[사용중인 포트번호]' 또는 'http://localhost:[사용중인 포트번호]'를 사용합니다.
origin: ['http://127.0.0.1:[사용중인 포트번호]', 'http://localhost:[사용중인 포트번호]'],
// `methods`: 허용할 HTTP 요청 메서드를 설정합니다.
methods: ['OPTIONS', 'POST', "GET", "DELETE"],
// `credentials`: 클라이언트와 서버 간에 자격 증명(쿠키 등)을 주고받을 수 있도록 허용합니다.
// 이 코드에서는 JWT를 쿠키가 아닌 `Authorization` 헤더로 주고받으므로, 이 설정은 필수는 아니지만,
// 이전 코드와의 일관성을 위해 유지될 수 있습니다.
credentials: true
}))
// `cookieParser` 미들웨어는 요청 헤더에 포함된 쿠키를 파싱하여 `req.cookies` 객체에 저장합니다.
// 현재 코드에서는 쿠키를 사용하지 않으므로 이 미들웨어는 필요하지 않습니다.
app.use(cookieParser())
// `express.json()` 미들웨어는 요청 본문에 JSON 형식의 데이터가 있을 경우 이를 파싱하여 `req.body` 객체에 저장합니다.
app.use(express.json())
// JWT 서명 및 검증에 사용될 비밀 키입니다.
// 실제 프로덕션 환경에서는 이 키를 환경 변수 등으로 관리하고, 더 길고 복잡하게 생성해야 합니다.
const secretKey = 'ozcodingschool'
// POST 요청 핸들러: 로그인 처리
app.post('/', (req, res) => {
const { userId, userPassword } = req.body // 요청 본문에서 userId와 userPassword를 추출합니다.
// 사용자 배열에서 제공된 ID와 비밀번호에 해당하는 사용자를 찾습니다.
const userInfo = users.find(el => el.user_id === userId && el.user_password === userPassword)
// 사용자를 찾지 못했거나 비밀번호가 일치하지 않을 경우
if (!userInfo) {
res.status(401).send('로그인 실패') // 401 Unauthorized 상태 코드와 메시지를 응답합니다.
} else {
// 사용자를 찾은 경우 JWT를 생성합니다.
// `jwt.sign()`: JWT를 생성하는 함수입니다.
// 첫 번째 인자: 페이로드 (JWT에 담을 데이터, 여기서는 사용자 ID)
// 두 번째 인자: 비밀 키
// 세 번째 인자: 옵션 (여기서는 토큰 만료 시간 10분)
const accessToken = jwt.sign({ userId: userInfo.user_id }, secretKey, { expiresIn: 1000 * 60 * 10 })
// 생성된 JWT를 클라이언트에 응답 본문으로 직접 전송합니다.
// 이 방식은 클라이언트가 토큰을 localStorage나 sessionStorage에 저장하여 관리하게 됩니다.
res.send(accessToken) // 생성된 토큰을 응답합니다.
}
})
// GET 요청 핸들러: 사용자 정보 조회 (인증 필요)
app.get('/', (req, res) => {
// `req.headers.authorization`: 클라이언트가 'Authorization' 헤더에 담아 보낸 토큰을 가져옵니다.
// 보통 'Bearer [토큰]' 형식으로 오므로, 'Bearer ' 부분을 제외하고 실제 토큰만 추출합니다.
const authHeader = req.headers.authorization;
// Authorization 헤더가 없거나 'Bearer ' 형식으로 시작하지 않으면 에러 처리
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).send('인증 토큰이 제공되지 않았거나 형식이 올바르지 않습니다.');
}
const accessToken = authHeader.split(' ')[1]; // 'Bearer ' 다음에 오는 실제 토큰 문자열을 추출합니다.
try {
// `jwt.verify()`: JWT를 검증하고 페이로드(payload)를 디코딩합니다.
// 첫 번째 인자: 검증할 JWT
// 두 번째 인자: 비밀 키
const payload = jwt.verify(accessToken, secretKey);
// 페이로드에서 추출한 userId를 사용하여 사용자 배열에서 해당 사용자 정보를 찾습니다.
const userInfo = users.find(el => el.user_id === payload.userId);
// 사용자 정보가 없으면 404 Not Found 응답을 보냅니다.
if (!userInfo) {
return res.status(404).send('사용자 정보를 찾을 수 없습니다.');
}
// 사용자 정보를 JSON 형태로 클라이언트에 응답합니다.
return res.json(userInfo);
} catch (error) {
// JWT 검증 중 오류가 발생한 경우 (예: 토큰 만료, 잘못된 서명)
if (error.name === 'TokenExpiredError') {
return res.status(401).send('토큰이 만료되었습니다.'); // 토큰 만료 에러 처리
}
return res.status(401).send('유효하지 않은 토큰입니다.'); // 그 외 유효하지 않은 토큰 에러 처리
}
})
// DELETE 요청 핸들러: 로그아웃 처리 (이 버전에서는 삭제됨)
// 이전 코드와 달리 DELETE 엔드포인트가 없습니다.
// JWT를 HTTP 헤더로 관리하는 방식에서는 클라이언트 측에서 토큰을 삭제하면 되므로,
// 서버 측에서 특별한 로그아웃 로직(예: 쿠키 삭제)이 필요 없을 수 있습니다.
// 하지만 서버 측에서 토큰 블랙리스트를 관리하는 경우에는 이 엔드포인트가 필요할 수 있습니다.
// 서버를 3000번 포트에서 실행합니다.
app.listen(3000, () => console.log('서버 실행!'))
const form = document.querySelector('form')
const idInput = document.querySelector('#user_id')
const passwordInput = document.querySelector('#user_password')
const loginButton = document.querySelector('#login_button')
const main = document.querySelector('main')
const userName = document.querySelector('#user_name')
const userInfo = document.querySelector('#user_info')
const logoutButton = document.querySelector('#logout_button')
// axios의 기본 설정으로, 모든 요청에 대해 자격 증명(쿠키, HTTP 인증 등)을 포함하도록 합니다.
// 이는 서버와의 세션 관리에 유용합니다.
axios.defaults.withCredentials = true;
let accessToken = '' // 로그인 성공 후 서버로부터 받은 accessToken을 저장할 변수입니다.
// 폼 제출 시 기본 동작(페이지 새로고침)을 막습니다.
form.addEventListener('submit', (e) => e.preventDefault())
// 로그인 함수: 사용자 ID와 비밀번호를 서버로 전송하여 로그인 요청을 보냅니다.
function login() {
const userId = idInput.value // ID 입력 필드의 값
const userPassword = passwordInput.value // 비밀번호 입력 필드의 값
// 'http://localhost:3000'으로 POST 요청을 보냅니다.
return axios.post('http://localhost:3000', { userId, userPassword })
// 요청 성공 시, 서버 응답(res.data)을 accessToken 변수에 저장합니다.
.then(res => accessToken = res.data)
}
// 로그아웃 함수: accessToken을 초기화하여 로그아웃 상태로 만듭니다.
// 실제 애플리케이션에서는 서버에도 로그아웃 요청을 보내 세션을 무효화하는 로직이 추가될 수 있습니다.
function logout() {
accessToken = ''
}
// 사용자 정보 조회 함수: 저장된 accessToken을 사용하여 서버에 사용자 정보 요청을 보냅니다.
function getUserInfo() {
return axios.get('http://localhost:3000', {
// 요청 헤더에 Authorization 필드를 추가하고, Bearer 토큰 방식으로 accessToken을 전송합니다.
headers: { 'Authorization' : `Bearer ${accessToken}`}
})
}
// 사용자 정보를 화면에 렌더링하는 함수:
// 메인 섹션을 보이게 하고, 로그인 폼을 숨깁니다.
// 사용자 이름과 정보를 해당하는 요소에 표시합니다.
function renderUserInfo(user) {
main.style.display = 'block' // 메인 섹션 표시
form.style.display = 'none' // 로그인 폼 숨김
userName.textContent = user.user_name // 사용자 이름 표시
userInfo.textContent = user.user_info // 사용자 정보 표시
}
// 로그인 폼을 화면에 렌더링하는 함수:
// 메인 섹션을 숨기고, 로그인 폼을 보이게 합니다.
// 이전에 표시되던 사용자 이름과 정보를 초기화합니다.
function renderLoginForm() {
main.style.display = 'none' // 메인 섹션 숨김
form.style.display = 'grid' // 로그인 폼 표시 (CSS 그리드 레이아웃)
userName.textContent = '' // 사용자 이름 초기화
userInfo.textContent = '' // 사용자 정보 초기화
}
// 로그인 버튼 클릭 시 실행되는 이벤트 핸들러:
loginButton.onclick = () => {
login() // 1. 로그인 함수 호출
.then(() => getUserInfo()) // 2. 로그인 성공 시, 사용자 정보 조회 함수 호출
.then(res => renderUserInfo(res.data)) // 3. 사용자 정보 조회 성공 시, 화면에 정보 렌더링
}
// 로그아웃 버튼 클릭 시 실행되는 이벤트 핸들러:
logoutButton.onclick = () => {
logout() // 1. 로그아웃 함수 호출 (accessToken 초기화)
renderLoginForm() // 2. 로그인 폼을 다시 화면에 표시
}