μΏ ν‚€λŠ” μ•„μ£Ό μž‘μ€ 데이터λ₯Ό μ €μž₯ν•  수 μžˆλŠ” λΈŒλΌμš°μ €κ°€ κ°–λŠ” μ €μž₯ 곡간 쀑 ν•˜λ‚˜λ‘œ λ™μΌν•œ μ„œλ²„μ— 접속할 λ•Œ μ €μž₯ν•œ μΏ ν‚€ 정보λ₯Ό 보낸닀.

HTTP protocol μžμ²΄λŠ” stateless인데 μΏ ν‚€κ°€ 정보λ₯Ό μ €μž₯ν•˜κ³  μžˆλ‹€ μ „μ†‘ν•¨μœΌλ‘œμ¨ 이λ₯Ό stateful ν•˜κ²Œ λ§Œλ“€μ–΄μ£ΌλŠ” 것이닀.

μΏ ν‚€λŠ” 주둜 λ‹€μŒκ³Ό 같은 μ„Έ 가지 κΈ°λŠ₯을 ν•œλ‹€.

  • Session management: 둜그인, μ‡Όν•‘ μΉ΄νŠΈμ™€ 같은 μ„œλ²„κ°€ μ €μž₯ν•΄μ•Όν•˜λŠ” 정보.
  • Personalization: μ‚¬μš©μž μ„ ν˜Έ, ν…Œλ§ˆμ™€ 같은 μ„€μ •κ°’.
  • Tracking: μ‚¬μš©μž 행동 뢄석을 μœ„ν•œ 기둝.

κ³Όκ±°μ—λŠ” ν΄λΌμ΄μ–ΈνŠΈμ— 무언가 μ €μž₯ν•  ν•„μš”κ°€ μžˆμ„ λ•Œ λͺ¨λ“  것을 쿠킀에 μ €μž₯ν–ˆλ‹€. μΏ ν‚€κ°€ ν΄λΌμ΄μ–ΈνŠΈμ— μ €μž₯ν•  수 μžˆλŠ” μœ μΌν•œ λ°©λ²•μ΄μ—ˆκΈ° λ•Œλ¬Έμ΄λ‹€. ν•˜μ§€λ§Œ μ˜€λŠ˜λ‚ μ€ ν΄λΌμ΄μ–ΈνŠΈμ— 정보 μ €μž₯ λͺ©μ μœΌλ‘œ μΏ ν‚€λ₯Ό μ‚¬μš©ν•˜λŠ” 것은 ꢌμž₯λ˜μ§€ μ•ŠλŠ”λ‹€. λͺ¨λ“  μš”μ²­λ§ˆλ‹€ μΏ ν‚€κ°€ ν•¨κ»˜ μ „μ†‘λ˜κΈ° λ•Œλ¬Έμ΄λ‹€. 특히 이것은 λͺ¨λ°”μΌμ—μ„œ μ„±λŠ₯이 λ–¨μ–΄μ§€λŠ” 원인이 될 수 μžˆλ‹€. λ”°λΌμ„œ 일반적인 정보 μ €μž₯이 λͺ©μ μ΄λΌλ©΄ Modern APIs 인 Web Storage API (localStorage, sessionStorage) λ˜λŠ” IndexedDB API λ₯Ό μ‚¬μš©ν•˜λŠ” 것이 ꢌμž₯λœλ‹€.

2. JavaScript Access

κ·ΈλŸΌμ—λ„ λΆˆκ΅¬ν•˜κ³  μΏ ν‚€λŠ” 아직도 μ‚¬μš©λ˜κ³  μžˆλ‹€. μ„œλ²„μ—μ„œ μ„Έμ…˜ 정보λ₯Ό 전솑할 λ•Œ μ‚¬μš©λ˜κΈ°λ„ ν•˜κ³ , 주둜 많이 μ‚¬μš©λ˜λŠ” 곳은 κ΄‘κ³ μ—…κ³„μ—μ„œ μ„œλ“œ νŒŒν‹° μΏ ν‚€κ°€ μ‚¬μš©μžμ˜ 행동을 μΆ”μ ν•˜λŠ” 것이닀. 물둠… μš”μ¦˜μ€ 이것도 μ• ν”Œμ˜ 크둜슀 μ‚¬μ΄νŠΈ 좔적 방지와 κ΅¬κΈ€μ˜ μ„œλ“œ νŒŒν‹° μΏ ν‚€ 차단에 μ˜ν•΄μ„œ 많이 λ§‰ν˜”μ§€λ§Œ μ„œλ²„μ—μ„œ μ„Έμ…˜ 관리λ₯Ό μœ„ν•΄ 쿠킀에 μ‹€μ–΄ λ³΄λ‚΄λŠ” 것은 μ—¬μ „νžˆ 많이 μ‚¬μš©λ˜κ³  μžˆλ‹€.

μΏ ν‚€λŠ” λ‹€μŒκ³Ό 같은 νŠΉμ§•μ„ κ°–λŠ”λ‹€.

  • 도메인 λ‹¨μœ„λ‘œ μ €μž₯
  • λΈŒλΌμš°μ €λ§ˆλ‹€ λ‹€λ₯΄μ§€λ§Œ ν‘œμ€€μ•ˆ κΈ°μ€€μœΌλ‘œ μ‚¬μ΄νŠΈλ‹Ή μ΅œλŒ€ 20개 및 4KB둜 μ œν•œ
  • 영ꡬ μ €μž₯ λΆˆκ°€λŠ₯(session, expires, max-age λ‹¨μœ„λ‘œ μ €μž₯)


μΏ ν‚€λŠ” μ„œλ²„μ—μ„œ 데이터λ₯Ό μ‹€μ–΄ λ³΄λ‚΄λŠ”λ° μ‚¬μš©λ˜κΈ°λ„ ν•˜μ§€λ§Œ λΈŒλΌμš°μ €μ—μ„œ JavaScript λ₯Ό ν†΅ν•΄μ„œ μ œμ–΄ν•˜λŠ” 것도 κ°€λŠ₯ν•˜λ‹€.

documentμ—λŠ” cookieλΌλŠ” 객체가 있고, 여기에 μΏ ν‚€λ₯Ό μ €μž₯ν•  수 μžˆλ‹€. 쿠킀에 μ„€μ •ν•  수 μžˆλŠ” λŒ€ν‘œμ μΈ 값은 λ‹€μŒκ³Ό κ°™λ‹€.

  • domain: 유효 도메인 μ„€μ •
  • path: 유효 경둜 μ„€μ •
  • expires: 만료 λ‚ μ§œ(UTC Date) μ„€μ •
  • max-age: 만료 타이머(seconds) μ„€μ •

μΏ ν‚€λŠ” 영ꡬ μ €μž₯이 λΆˆκ°€λŠ₯ν•˜λ‹€κ³  ν–ˆλ‹€. expires λ˜λŠ” max-age λ₯Ό μ§€μ •ν•΄μ•Όν•˜λ©°, μ§€μ •ν•˜μ§€ μ•Šμ„ μ‹œ 기본값은 session으둜 μ„Έμ…˜μ΄ μœ μ§€λ˜λŠ” λ™μ•ˆμ—λ§Œ μœ νš¨ν•˜λ‹€.


쿠킀에 데이터λ₯Ό μ €μž₯ν•˜λŠ” 것은 JSON String 을 μ‚¬μš©ν•˜λ“― λ¬Έμžμ—΄λ‘œ ν•  수 있으며, Syntax μ²΄κ³„λŠ” λ³„λ„μ˜ λ”°μ˜΄ν‘œ 없이 ;둜 κ΅¬λΆ„ν•œλ‹€.

document.cookie = 'a=1; domain=localhost;';
document.cookie = 'b=2; domain=127.0.0.1;';
document.cookie = 'c=3; domain=google.com;';
document.cookie = 'd=4; domain=localhost; path=/';
document.cookie = 'e=5; domain=localhost; path=/abc';
document.cookie = 'f=6; domain=localhost; path=/*';

μœ„μ™€ 같이 μΏ ν‚€λ₯Ό μ €μž₯ν•œ μƒνƒœμ—μ„œ localhost/def둜 μ ‘μ†ν•΄λ³΄μž.

Cookie

총 6개의 μΏ ν‚€λ₯Ό μ €μž₯ν–ˆμ§€λ§Œ 4개만 μ €μž₯된 것을 확인할 수 μžˆλ‹€. b와 cλŠ” 도메인 뢈일치둜 μ €μž₯λ˜μ§€ μ•ŠλŠ” 것을 확인할 수 μžˆλ‹€.

이제 ν˜„μ œ domain/pathμ—μ„œ μœ νš¨ν•œ μΏ ν‚€κ°€ 무엇인지 좜λ ₯ν•΄λ³΄μž.

console.log(document.cookie)
a=1; d=4

a와 d만 μœ νš¨ν•œ 것을 μ•Œ 수 μžˆλ‹€. e와 fλŠ” pathκ°€ 달라 ν˜„μž¬ μ£Όμ†Œμ—μ„œλŠ” μ‚¬μš©ν•  수 μ—†λŠ” μΏ ν‚€λ‹€. μΏ ν‚€μ—μ„œμ˜ wildcard λŠ” *이 μ•„λ‹ˆλΌ /둜 끝내면 μ΄ν•˜ μ£Όμ†Œλ₯Ό λͺ¨λ‘ ν¬ν•¨ν•œλ‹€λŠ” 것을 μ•Œ 수 μžˆλ‹€.

μ΄λ²ˆμ—λŠ” localhost/abc둜 접속해 μœ νš¨ν•œ μΏ ν‚€λ₯Ό ν™•μΈν•΄λ³΄μž.

e=5; a=1; d=4

이제 e, a, dκ°€ μœ νš¨ν•œ μΏ ν‚€μž„μ„ μ•Œ 수 μžˆλ‹€. λ§ˆμ°¬κ°€μ§€λ‘œ localhost/*둜 μ ‘μ†ν•˜λ©΄ f, a, dκ°€ μœ νš¨ν•œ μΏ ν‚€κ°€ λœλ‹€.

참고둜 expires와 max-ageλŠ” λ‹€μŒκ³Ό 같이 λ“±λ‘ν•˜λ©΄ μ‰½κ²Œ μ‚¬μš©ν•  수 μžˆλ‹€.

const THREE_DAYS_SECONDS = 60 * 60 * 24 * 3;
document.cookie = `a=1; max-age=${THREE_DAYS_SECONDS}`;
document.cookie = `b=2; expires=${daysLaterMidnight(3).toUTCString()}`;

function daysLaterMidnight(days) {
  const now = new Date();
  const threeDaysLaterMidnight = new Date(
    now.getFullYear(),
    now.getMonth(),
    now.getDate() + days
  );
  return threeDaysLaterMidnight;
}


νŠΉμ • μΏ ν‚€μ˜ 값을 κ°€μ Έμ˜€κ±°λ‚˜ μ‚­μ œν•˜κΈ° μœ„ν•΄μ„œλŠ” λ‹€μŒκ³Ό 같이 ν•¨μˆ˜λ₯Ό λ§Œλ“€μ–΄ μ‚¬μš©ν•˜λ©΄ νŽΈλ¦¬ν•˜λ‹€.

export const getCookie = (name) => {
  const cookie = document.cookie
    .split('; ')
    .find((str) => str.split('=')[0] === name);
  return cookie ? cookie.split('=')[1] : null;
};

export const getCookies = (...names) => names.map(getCookie);
export const removeCookie = (name) =>
  (document.cookie = `${name}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC`);

export const removeCookies = (...names) => names.forEach(removeCookie);

3. HTTP Only & Secure

μœ„μ—μ„œ μΏ ν‚€κ°€ μ„Έμ…˜μ„ κ΄€λ¦¬ν•˜λŠ” 데 μ‚¬μš©λœλ‹€κ³  ν–ˆλ‹€. Session IDλŠ” 맀우 λ―Όκ°ν•œ κ°œμΈμ •λ³΄λ‹€. μ ˆλŒ€λ‘œ 유좜이 λ˜μ–΄μ„œλŠ” μ•ˆ λœλ‹€. 그런데 방금 JavaScript 둜 μΏ ν‚€λ₯Ό λ„ˆλ¬΄λ„ μ†μ‰½κ²Œ λ‹€λ£¨λŠ” 것을 ν™•μΈν–ˆλ‹€.

μΏ ν‚€μ˜ JavaScript 의 접근은 μΏ ν‚€μ˜ 데이터에 HttpOnlyλ₯Ό μΆ”κ°€ν•˜λ©΄ JavaScript 의 접근을 막을 수 μžˆλ‹€. 이러면 더이상 XSS 둜 μΈν•œ μΏ ν‚€ νƒˆμ·¨λ₯Ό 차단할 수 μžˆλ‹€.

이 방법은 λΈŒλΌμš°μ €μ—μ„œμ˜ μΏ ν‚€ νƒˆμ·¨λŠ” 막을 수 μžˆμ§€λ§Œ, μ™€μ΄νŒŒμ΄λ₯Ό κ°μ²­ν•˜κ±°λ‚˜ 심지어 ISP 케이블을 κ°μ²­ν•˜λŠ” λ“± λ„€νŠΈμ›Œν¬ 자체λ₯Ό 감청해 μΏ ν‚€λ₯Ό νƒˆμ·¨ν•˜λŠ” 것은 막을 수 μ—†λ‹€. λ¬Όλ‘ , 이λ₯Ό μœ„ν•œ λŒ€μ±…λ„ μ‘΄μž¬ν•œλ‹€. λ°”λ‘œ secureλ₯Ό μΆ”κ°€ν•˜λŠ” 것이닀. 이 값이 μΆ”κ°€λ˜λ©΄ 이제 μΏ ν‚€λŠ” SSL/TLS λ₯Ό μ‚¬μš©ν•  λ•Œλ§Œ μ „μ†‘λ˜λ„λ‘ μ„€μ •λ˜μ–΄ HTTPS ν†΅μ‹ μœΌλ‘œ μ•”ν˜Έν™”λœ μ „μ†‘λ§Œμ„ ν—ˆμš©ν•œλ‹€. κ°œλ°œμžκ°€ μ‹€μˆ˜λ‘œ HTTP 둜 μž‘μ„±ν•  경우 μΏ ν‚€μ˜ 전솑 자체λ₯Ό μ°¨λ‹¨ν•˜λŠ” 것이닀.

HTTP Only Cookies, Secure Cookies λŠ” λ™μ‹œμ— μ„€μ •ν•  수 있으며, 이λ₯Ό 톡해 λŒ€λΆ€λΆ„μ˜ μΏ ν‚€ νƒˆμ·¨ 문제λ₯Ό ν•΄κ²°ν•  수 μžˆλ‹€.

const express = require('express');
const cookieParser = require('cookie-parser');

const app = express();
const PORT = 3000;

app.use(cookieParser());

app.get('/', (req, res) => {
  res.cookie('myCookie', 'Hello, World!', {
    httpOnly: true,
    secure: true
  });

  res.send('Cookie with HttpOnly and Secure flag has been set.');
});

app.listen(PORT, () => {
  console.log(`Server is running on http://localhost:${PORT}`);
});

2. Session πŸ‘©β€πŸ’»

1. Session

μΏ ν‚€μ˜ μš©λ„ 쀑 ν•˜λ‚˜κ°€ μ„Έμ„  정보λ₯Ό 전솑할 λ•Œ μ‚¬μš©ν•œλ‹€κ³  ν–ˆλ‹€. μ„Έμ…˜μΌλž€ λ¬΄μ—‡μΌκΉŒ? HTTP κ°€ stateless 이기 λ•Œλ¬Έμ— μ‚¬μš©μžμ˜ 인증 정보λ₯Ό μœ μ§€ν•˜κΈ° μœ„ν•΄μ„  이 μƒνƒœλ₯Ό μ €μž₯ν•  ν•„μš”κ°€ μžˆλ‹€. 이 방법 쀑 ν•˜λ‚˜κ°€ λ°”λ‘œ μ„Έμ…˜μ΄λ‹€.

μ„Έμ…˜μ΄ λ¬Όλ‘  아직도 μ‚¬μš©λ˜κ³ λŠ” μžˆμ§€λ§Œ μž μ‹œ ν›„ 이야기 ν•  Token 에 λΉ„ν•˜λ©΄ μ’€ 더 전톡적인 방식이라 ν•  수 μžˆλ‹€.

Session

  1. ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ— 둜그인 μš”μ²­.
  2. μ„œλ²„λŠ” λ‘œκ·ΈμΈμ„ 처리(authentication), μ„Έμ…˜μ„ DB에 μ €μž₯ν•˜κ³  Session ID κ°€ λ‹΄κΈ΄ μΏ ν‚€λ₯Ό ν΄λΌμ΄μ–ΈνŠΈμ— λ°˜ν™˜.
  3. ν΄λΌμ΄μ–ΈνŠΈλŠ” 쿠킀와 ν•¨κ»˜ μ„œλ²„μ— ν•„μš”ν•œ μž‘μ—…μ„ μš”μ²­.
  4. μ„œλ²„λŠ” μΏ ν‚€μ˜ Session ID λ₯Ό μ €μž₯ν•œ DB 와 비ꡐ해 μœ νš¨ν•œμ§€ 검증. μœ νš¨ν•œ μ„Έμ…˜μ΄ ν™•μΈλ˜λ©΄ μ‚¬μš©μž μš”μ²­μ„ μ²˜λ¦¬ν•΄ 응닡.

2. Limitation of Session

μ„Έμ…˜μ€ μ„œλ²„κ°€ μ €μž₯ν•˜κ³  κ΄€λ¦¬ν•˜κΈ° λ•Œλ¬Έμ— ν΄λΌμ΄μ–ΈνŠΈμ™€ λ„€νŠΈμ›Œν¬ κ³Όμ •μ—μ„œ νƒˆμ·¨λ‘œ μΈν•œ XSS, CSRF 곡격을 받을 수 μžˆλ‹€λŠ” κ²ƒλ§Œ μ‘°μ‹¬ν•˜λ©΄ μ•ˆμ „μ„±μ΄ 맀우 λ†’λ‹€. ν•˜μ§€λ§Œ μš”μ¦˜μ€ JWT, OAuth 같은 Token 이 많이 μ–ΈκΈ‰λ˜κ³  μ‚¬μš©λœλ‹€. μ™œ μ˜€λž«λ™μ•ˆ 잘 μ‚¬μš©ν–ˆκ³ , μ—¬μ „νžˆ λ§Žμ€ 곳에 μ‚¬μš©μ€‘μΈ μ„Έμ…˜μ˜ μƒλ‹Ήμˆ˜λŠ” 토큰에 λŒ€μ²΄λ˜λŠ” κ²ƒμΌκΉŒ?

μ–΄λŠ νšŒμ‚¬μ˜ μ„œλΉ„μŠ€κ°€ κΈ°λŠ₯이 λ§Žμ•„μ Έ μ„œλ²„κ°€ μˆ˜μš©ν•  수 μžˆλŠ” μ²˜λ¦¬λŸ‰μ˜ ν•œκ³„κ°€ λ„˜μ–΄μ„œλ©΄ μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ? 더 높은 μ»΄ν“¨νŒ… νŒŒμ›Œκ°€ ν•„μš”ν•˜λ‹€. νšŒμ‚¬μ˜ μ„œλΉ„μŠ€κ°€ 잘 νŒ”λ € μ‚¬μš©μžκ°€ λ§Žμ•„μ Έ μ²˜λ¦¬λŸ‰μ˜ ν•œκ³„λ₯Ό λ„˜μ–΄μ„œλ©΄ μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ? λ§ˆμ°¬κ°€μ§€λ‘œ 더 높은 ν…€ν“¨νŒ… νŒŒμ›Œκ°€ ν•„μš”ν•˜λ‹€.

더 높은 μ»΄ν“¨νŒ… μ„±λŠ₯을 μ–»κΈ° μœ„ν•΄μ„œλŠ” μ–΄λ–»κ²Œ ν•΄μ•Όν• κΉŒ? μš°μ„  κ°€μž₯ μ‰¬μš΄ 방법은 μ„œλ²„μ˜ μ„±λŠ₯을 λ†’μ΄λŠ” 것이닀. κ³ μ„±λŠ₯ μ„œλ²„λ‘œ κ΅μ²΄ν•œλ‹€κ±°λ‚˜ AWS 와 같은 ν΄λΌμš°λ“œλ₯Ό μ‚¬μš©ν•œλ‹€λ©΄ 더 높은 μ»΄ν“¨νŒ… μ„±λŠ₯으둜 μ—…κ·Έλ ˆμ΄λ“œ μš”μ²­μ„ ν•˜λŠ” 것이닀. 이런 ν™•μž₯ 방법을 Scare-Up 이라 ν•œλ‹€.

ν•˜μ§€λ§Œ λ¬Έμ œλŠ” 단일 μ»΄ν“¨ν„°μ˜ μ„±λŠ₯을 λ†’μ΄λŠ” 것은 λ‹€μŒκ³Ό 같은 ν•œκ³„λ₯Ό κ°–λŠ”λ‹€.

  • μ„±λŠ₯이 λ†’μ•„μ§ˆμˆ˜λ‘ ν–₯상폭 λŒ€λΉ„ λΉ„μš© 증가가 훨씬 크닀.
  • 단일 μ»΄ν“¨ν„°μ˜ μ„±λŠ₯은 ν•œκ³„κ°€ μ‘΄μž¬ν•œλ‹€.
  • μ„œλ²„κ°€ λ‹€μš΄λ˜λ©΄ 전체 μ„œλΉ„μŠ€κ°€ μ£½λŠ”λ‹€.


그러면 μ–΄λ–»κ²Œ ν•΄μ•Ό λΉ„μš©λ„ 쀄이고, μ»΄ν“¨νŒ… μ„±λŠ₯의 ν•œκ³„λ„ κ·Ήλ³΅ν•˜λ©°, μ„œλΉ„μŠ€κ°€ μ€‘λ‹¨λ˜λŠ” μ΅œμ•…μ˜ μ‚¬νƒœλ₯Ό 막을 수 μžˆμ„κΉŒ?

λΆ„μ‚° μ»΄ν“¨νŒ…μ„ μ‚¬μš©ν•˜λŠ” 것이닀. μš”μ¦˜ λ°±μ—”λ“œμ—μ„œ 많이 μ‚¬μš©ν•˜λŠ” MSA(Microservice Architecture) μ—­μ‹œ λΆ„μ‚° μ»΄ν“¨νŒ…μ„ ν™œμš©ν•œ 것이라 ν•  수 μžˆλ‹€.

Load Balancer

λ‘œλ“œ λ°ΈλŸ°μ„œλ₯Ό Proxy 둜 두고 뒀에 μ—¬λŸ¬ 개의 μ„œλ²„κ°€ μ‘΄μž¬ν•˜λ©΄ 인터넷 μƒμ—μ„œλŠ” ν•˜λ‚˜μ˜ μ„œλ²„μ²˜λŸΌ λ³΄μ΄μ§€λ§Œ μ—¬λŸ¬ 개의 μ„œλ²„κ°€ 역할을 λΆ„λ‹΄ν•  수 있게 λœλ‹€. λΉ„μš© λŒ€λΉ„ μ„±λŠ₯ 증가가 효율적이며, μ„œλ²„λ₯Ό 좔가함에 따라 μ„±λŠ₯이 거의 μ •λΉ„λ‘€ν•˜κ²Œ 였λ₯Έλ‹€. λ˜ν•œ μ—¬λŸ¬ μ„œλ²„κ°€ μ‘΄μž¬ν•˜κΈ° λ•Œλ¬Έμ— ν•˜λ‚˜μ˜ μ„œλ²„κ°€ κ³ΌλΆ€ν•˜λ‘œ λ‹€μš΄λ˜λ”λΌλ„ λ‹€λ₯Έ μ„œλ²„κ°€ 있기 λ•Œλ¬Έμ— 전체 μ„œλΉ„μŠ€κ°€ 죽지 μ•ŠλŠ”λ‹€. μž‘μ€ μ„œλΉ„μŠ€ λ³„λ‘œ λΆ„μ‚°ν–ˆμ„ 경우 νŠΉμ • μ„œλΉ„μŠ€λ§Œ μ£½κ±°λ‚˜, λ™μΌν•œ μ„œλ²„λ₯Ό μ—¬λŸ¬ 개 λΆ„μ‚°ν–ˆμ„ 경우 λ‹€μš΄λœ μ„œλ²„κ°€ μž¬λΆ€νŒ… 되고 정상화 λ˜λŠ” λ™μ•ˆλ§Œ λ‹€λ₯Έ μ„œλ²„κ°€ 버텨주면 λœλ‹€. λ˜ν•œ μ‹€μ œ μ„œλ²„λ₯Ό μ™ΈλΆ€ λ„€νŠΈμ›Œν¬μ— λ…ΈμΆœμ‹œν‚€μ§€ μ•ŠμŒμœΌλ‘œμ¨ λ§ˆμŠ€ν‚Ή 효과λ₯Ό μΆ”κ°€ν•΄ λ³΄μ•ˆμ΄ κ°•ν™”λ˜λŠ” 것은 덀으둜 μ–»κ²Œ λ˜λŠ” νš¨κ³Όλ‹€. 이런 ν™•μž₯ 방법을 Scale-Out 이라 ν•œλ‹€.

μ–΄λ–»κ²Œ λ‘œλ“œλ°ΈλŸ°μ„œκ°€ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό λΆ„μ‚°μ‹œν‚€λŠ”μ§€, λ‘œλ“œλ°ΈλŸ°μ‹± 효과λ₯Ό 높이기 μœ„ν•΄ μ–΄λ–»κ²Œ ν•΄μ•Όν•˜λŠ”μ§€μ™€ 같은 λ¬Έμ œλŠ” λ¬΄μ‹œν•˜κ³  μ„Έμ…˜μ— λŒ€ν•΄μ„œλ§Œ μ΄μ•ΌκΈ°ν•΄λ³΄μž.
μ„Έμ…˜μ€ μ„œλ²„μ—μ„œ κ΄€λ¦¬ν•œλ‹€κ³  ν–ˆλ‹€. κ·Έλ ‡λ‹€λ©΄ ν΄λΌμ΄μ–ΈνŠΈμ˜ μ„Έμ…˜μ΄ μœ μ§€κ°€ 되기 μœ„ν•΄μ„œλŠ” μ„œλ²„κ°€ 항상 ν΄λΌμ΄μ–ΈνŠΈμ˜ μ„Έμ…˜ 정보λ₯Ό μ•Œκ³  μžˆμ–΄μ•Ό ν•œλ‹€λŠ” λ¬Έμ œκ°€ 생긴닀. 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•œ λ°©λ²•μ—λŠ” μ–΄λ–€ 것이 μžˆμ„κΉŒ?


1 ) Sticky Session

첫째, λ‘œλ“œλ°ΈλŸ°μ„œκ°€ ν΄λΌμ΄μ–ΈνŠΈμ˜ 정보λ₯Ό κΈ°μ–΅ν•΄ 맀번 λ™μΌν•œ μ„œλ²„λ‘œ μ—°κ²°μ‹œν‚€λŠ” 것이닀. ν•˜μ§€λ§Œ 이 방법은 κ²°κ΅­ λ‘œλ“œλ°ΈλŸ°μ„œμ˜ 뢀담이 μ»€μ§€κ²Œ λœλ‹€. μ„œλ²„μ˜ 뢀담을 μ€„μ΄κ³ μž λΆ„μ‚° μ„œλ²„λ₯Ό λ„μž…ν•˜κ³  λ‘œλ“œλ°ΈλŸ°μ„œλ₯Ό λ‘μ—ˆλŠ”λ° 이제 λ‘œλ“œλ°ΈλŸ°μ„œλ₯Ό ν™•μž₯ν•΄μ•Όν•˜λŠ” 상황이 λ˜μ–΄λ²„λ¦° 것이닀.

λ‘˜μ§Έ, 쿠킀에 μ„Έμ…˜μ„ 응닡할 λ•Œ μ–΄λ–€ μ„œλ²„μ™€ μ—°κ²°λ˜μ—ˆλŠ”μ§€ 정보λ₯Ό μΆ”κ°€ν•΄ λ‘œλ“œλ°ΈλŸ°μ„œκ°€ 이λ₯Ό ν™•μΈν•˜κ³  λ™μΌν•œ μ„œλ²„λ‘œ μ—°κ²°μ‹œν‚€λŠ” 것이닀. λ‘œλ“œλ°ΈλŸ°μ„œκ°€ μ—°κ²°ν•  μ„œλ²„λ₯Ό κΈ°μ–΅ν•  ν•„μš” 없이 μΏ ν‚€λ₯Ό ν™•μΈλ§Œ ν•˜λ©΄ λ˜λ‹ˆ 뢀담이 쀄어든닀. ν•˜μ§€λ§Œ μ„œλ²„ λ§ˆμŠ€ν‚Ή νš¨κ³Όκ°€ 사라진닀.

그리고 μ–΄λ–€ 방법을 μ‚¬μš©ν•˜λ˜ μ—°κ²°λœ μ„œλ²„κ°€ λ‹€μš΄λ  경우 μ„œλΉ„μŠ€κ°€ μ£½μ§€λŠ” μ•Šμ§€λ§Œ ν΄λΌμ΄μ–ΈνŠΈλŠ” μ„Έμ…˜μ„ μžƒμ–΄λ²„λ¦¬κ²Œ λ˜λŠ” λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.


2 ) Session Clustering

μ„œλ²„μ— μ„Έμ…˜μ΄ 생성, λ³€κ²½, 제거될 λ•Œ λ‹€λ₯Έ μ„œλ²„μ— μ „νŒŒν•΄ λͺ¨λ“  μ„œλ²„κ°€ λ™μΌν•œ μ„Έμ…˜μ„ μ†Œμœ ν•˜λŠ” 것이닀. 이 방법은 μœ„ Sticky Session 이 κ°–λŠ” λͺ¨λ“  문제λ₯Ό ν•΄κ²°ν•˜μ§€λ§Œ λ‹€λ₯Έ λ¬Έμ œκ°€ λ°œμƒν•œλ‹€.

λΆ„μ‚° μ„œλ²„κ°€ μ—¬λŸ¬ λŒ€μΌ 경우 λͺ¨λ“  μ„œλ²„μ— μ„Έμ…˜μ„ μ „νŒŒν•΄μ•Όν•˜λ―€λ‘œ λ„€νŠΈμ›Œν¬ λΉ„μš©μ΄ μ¦κ°€ν•œλ‹€. μ‚¬μš©μž μ¦κ°€λ‘œ μΈν•œ μ„œλ²„ 뢀담을 쀄이기 μœ„ν•΄ λΆ„μ‚° μ„œλ²„λ₯Ό μ‚¬μš©ν•˜μ§€λ§Œ μ„Έμ…˜μ€ κ²°κ΅­ Scale-Up 을 ν–ˆμ„ λ•Œμ™€ 달라진 것이 μ—†λ‹€. λͺ¨λ“  μ„œλ²„κ°€ λ™μΌν•˜κ²Œ λͺ¨λ“œ μ„Έμ…˜μ„ κ°€μ Έμ•Όν•œλ‹€. μ΄λŠ” κ²°κ΅­ μ„Έμ…˜μœΌλ‘œ μΈν•œ μ„œλ²„ 뢀담을 μ¦κ°€μ‹œν‚€λŠ” 문제λ₯Ό λ°œμƒμ‹œν‚¨λ‹€. λ˜ν•œ μ„Έμ…˜μ΄ 동기화 되기 이전에 ν΄λΌμ΄μ–ΈνŠΈκ°€ λ‹€λ₯Έ μ„œλ²„λ‘œ 접속될 경우 μ„Έμ…˜μ„ μžƒμ€ 것과 같은 상황이 λ°œμƒν•œλ‹€λŠ” λ¬Έμ œκ°€ μžˆλ‹€.


3 ) In-Memory Session Storage

졜근 κ°€μž₯ 많이 μ‚¬μš©λ˜λŠ” 것은 μ„Έμ…˜λ§Œμ„ κ΄€λ¦¬ν•˜κΈ° μœ„ν•œ λ³„λ„μ˜ μ„œλ²„λ₯Ό μΆ”κ°€ν•˜λŠ” 것이닀. 기쑴의 μ„Έμ…˜μ€ DB 에 μ €μž₯ν•˜κΈ° λ•Œλ¬Έμ— DB 의 λΆ€λ‹΄ λ˜ν•œ 컸으며, λŒ€λΆ€λΆ„μ˜ μ„œλ²„κ°€ RDBMS λ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ— λŠλ¦¬κΈ°κΉŒμ§€ ν–ˆλ‹€.

Redis와 MemcachedλŠ” In-Memory 캐싱 μ„œλ²„λ₯Ό ꡬ좕해 μ„Έμ…˜μ„ μ €μž₯ν•˜κ³  κ΄€λ¦¬ν•œλ‹€. λ”°λΌμ„œ 맀우 λΉ λ₯Έ μ†λ„λ‘œ μ„Έμ…˜μ„ 관리할 수 μžˆμ„ 뿐 μ•„λ‹ˆλΌ, 더이상 λ‘œλ“œλ°ΈλŸ°μ„œλŠ” μ„Έμ…˜μ„ ꡬ뢄할 ν•„μš” 없이 λ‘œλ“œλ°ΈλŸ°μ‹±μ΄λΌλŠ” 본래의 λͺ©μ μ—λ§Œ μ§‘μ€‘ν•˜λ©΄ 되고, μ„œλ²„μ™€ DB μ—­μ‹œ μ„Έμ…˜ μ„œλ²„μ—κ²Œ 질의만 ν•˜λ©΄ 되기 λ•Œλ¬Έμ— 뢀담이 쀄어든닀. λ˜ν•œ μœ„ Session Clustering κ³Ό 같이 동기화 λ¬Έμ œλ„ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ”λ‹€. μ„Έμ…˜μ„ μ‚¬μš©ν•˜λŠ” MSA μ‹œμŠ€ν…œμ—μ„œ μ„Έμ…˜μ„ κ΄€λ¦¬ν•˜λŠ” κ°€μž₯ 보편적인 방법이닀.


이제 μš°λ¦¬λŠ” μ„Έμ…˜μ΄ μœ μ§€λ˜μ§€ μ•Šμ„ 수 μžˆλŠ” λ¬Έμ œμ— λŒ€ν•΄μ„œλŠ” 해결을 ν–ˆλ‹€. ν•˜μ§€λ§Œ μ ‘μ†μžκ°€ μ¦κ°€ν•˜λ©΄ 관리해야할 μ„Έμ…˜μ΄ μ¦κ°€ν•˜κ³ , μ–΄μ°Œ λ˜μ—ˆλ“  μ„Έμ…˜ μ„œλ²„μ˜ 뢀담이 μ»€μ§€λŠ” 것은 막을 수 μ—†λ‹€. μ„Έμ…˜ μ„œλ²„μ˜ κ΅¬μž…μ΄λΌλŠ” μΆ”κ°€ λΉ„μš© λ˜λŠ” AWS 와 같은 ν΄λΌμš°λ“œ ν™˜κ²½μ„ μ΄μš©ν•  경우 μΆ”κ°€ λΉ„μš©μ΄ μ²­κ΅¬λ˜λŠ” 것은 자체 μ„œλ²„μ™€ λ§‰λŒ€ν•œ 인프라와 인λ ₯을 μš΄μš©ν•˜λŠ” IT λŒ€κΈ°μ—…μ΄ μ•„λ‹Œ 이상 λΉ„μš© μΆ”κ°€λ₯Ό κ°λ‹Ήν•΄μ•Όν•œλ‹€.


3. Token πŸ‘©β€πŸ’»

1. Public Key & Private Key

Session 이 μ„œλ²„μ— 뢀담을 μ£ΌλŠ” 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ 인증 정보λ₯Ό μ„œλ²„κ°€ μ•„λ‹Œ ν΄λΌμ΄μ–ΈνŠΈμ— μ €μž₯ν•˜κ³  κ΄€λ¦¬ν•˜λŠ” 토큰 방식이 많이 λ„μž…λ˜κ³ μžˆλ‹€. 이 토큰 방식은 μ–΄λ–»κ²Œ μž‘λ™ν•˜κΈΈλž˜ ν΄λΌμ΄μ–ΈνŠΈμ— μ €μž₯ν•˜κ³  관리해도 μ•ˆμ „ν•œ κ²ƒμΌκΉŒ? 토큰을 μ΄ν•΄ν•˜κΈ° μœ„ν•΄μ„œλŠ” μš°μ„  Public Key 와 Private Key λ₯Ό μ΄ν•΄ν•΄μ•Όν•œλ‹€.

μ•”ν˜Έν™”λŠ” 크게 ν•˜λ‚˜μ˜ ν‚€λ‘œ μ•”ν˜Έν™”(Encryption)κ³Ό λ³΅ν˜Έν™”(Decryption)을 λͺ¨λ‘ ν•  수 μžˆλŠ” λŒ€μΉ­ν‚€(Symmetric Key)와 두 개의 ν‚€κ°€ ν•˜λ‚˜μ˜ 쌍이 λ˜μ–΄ ν•˜λ‚˜μ˜ ν‚€λ‘œ μ•”ν˜Έν™”λ₯Ό ν•˜λ©΄ λ°˜λŒ€μ˜ ν‚€λ‘œλ§Œ 볡ꡬ할 수 μžˆλŠ” λΉ„λŒ€μΉ­ν‚€(Asymmetric Key)κ°€ μžˆλ‹€. 이 λΉ„λŒ€μΉ­ν‚€μ˜ μŒμ„ μ΄λ£¨λŠ” ν‚€ 쀑 ν•˜λ‚˜λ₯Ό Public Key 라 ν•˜κ³ , λ‹€λ₯Έ ν•˜λ‚˜λ₯Ό Private Key 라 ν•œλ‹€.

λ‹¨μˆœ λΉ„λŒ€μΉ­ν‚€λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

Aν‚€ Bν‚€
Aν‚€λ‘œ 데이터λ₯Ό μ•”ν˜Έν™” Aν‚€λ‘œ μ•”ν˜Έν™” ν•œ 데이터λ₯Ό Bν‚€λ‘œ λ³΅ν˜Έν™”
Bν‚€λ‘œ μ•”ν˜Έν™” ν•œ 데이터λ₯Ό Aν‚€λ‘œ λ³΅ν˜Έν™” Bν‚€λ‘œ 데이터λ₯Ό μ•”ν˜Έν™”

λ”°λΌμ„œ 기본적으둜 ν•˜λ‚˜μ˜ ν‚€ μŒμ—μ„œ 두 ν‚€λŠ” μ„œλ‘œ λ™μΌν•œ ꡬ쑰λ₯Ό κ°–κ²Œ λœλ‹€. ν‚€ μŒμ„ λ§Œλ“€μ–΄ ν•˜λ‚˜λ₯Ό λ°°ν¬ν•˜λŠ” μˆœκ°„ λ°°ν¬λ˜μ§€ μ•Šμ€ μͺ½μ΄ Private Key κ°€ λ˜λŠ” 것이고, 배포된 μͺ½μ΄ Public Key κ°€ λ˜λŠ” ꡬ쑰닀.

ν•˜μ§€λ§Œ ν„°λ―Έλ„λ‘œ ν‚€λ₯Ό 생성해보면 Private Key κ°€ Public Key μš©λŸ‰μ΄ 훨씬 크게 μƒμ„±λœλ‹€. μ™œ 그럴까? 일단, Private Key κ°€ 더 μ€‘μš”ν•˜κ³ , Private Key μ—λ§Œ ν•„μš”ν•œ κΈ°λŠ₯이 μΆ”κ°€λ˜κΈ° λ•Œλ¬Έμ΄λ‹€.

Private Key 의 첫 번째 차이

Public Key λŠ” κ³΅κ°œλ˜μ–΄ μ „μ†‘λœλ‹€. λ‚˜μ˜ 신원을 증λͺ…ν•˜κΈ°λ₯Ό μ›ν•˜λŠ” 곳에 μˆ˜μ—†μ΄ 많이 λ³΅μ œλ˜μ–΄ 전솑될 것이닀. μ•Œκ³ λ¦¬μ¦˜μ΄ 크고 무거울 수둝 κ°•λ ₯ν•΄μ§€μ§€λ§Œ μš©λŸ‰μ΄ μ¦κ°€ν•˜κΈ° λ•Œλ¬Έμ— μ λ‹Ήν•œ μ•”ν˜Έν™” μ„±λŠ₯에 μ λ‹Ήν•œ 크기λ₯Ό κ°–λŠ” 것이 μ’‹λ‹€.

반면 Private Key λŠ” 개인 λ˜λŠ” κΈ°μ—… 단일 κ°œμ±„κ°€ 가지고 있기 λ•Œλ¬Έμ— 신원 증λͺ…μœΌλ‘œ μ‚¬μš©ν•  수 μžˆλ‹€. 신원을 증λͺ…ν•œλ‹€λŠ” 것은 λ‹¨μˆœνžˆ 데이터λ₯Ό μ•”ν˜Έν™” ν•˜λŠ” 것보닀 더 μ€‘μš”ν•œ κΈ°λŠ₯이닀. λ”°λΌμ„œ μ–΄λ”” 전솑할 ν•„μš”λ„ 없을 뿐 μ•„λ‹ˆλΌ 더 κ°•λ ₯ν•œ μ•Œκ³ λ¦¬μ¦˜μ„ μ μš©ν•΄ 신원 확인 κΈ°λŠ₯을 κ°•ν™”ν•˜λŠ” 것이 μ’‹κΈ° λ•Œλ¬Έμ— 더 크고 무거운 μ•Œκ³ λ¦¬μ¦˜μ„ μ‚¬μš©ν•œλ‹€.

λ”°λΌμ„œ 일반적으둜 Public Key λŠ” RSA 1024~4096 bit μ •λ„μ˜ μ•Œκ³ λ¦¬μ¦˜μ„ μ‚¬μš©ν•˜κ³ , Private Key λŠ” RSA 2048~8192 bit 의 μ•Œκ³ λ¦¬μ¦˜μ„ μ‚¬μš©ν•œλ‹€. 이게 Public Key 와 Private Key 의 첫 번째 차이닀.


Private Key 의 두 번째 차이

μΈμ¦μ„œ λ°œκΈ‰κΈ°κ΄€ λ“± μ•žμ„œ μΌμ–΄λ‚˜λŠ” μ ˆμ°¨λŠ” 일단 λ¬΄μ‹œν•˜κ³  μ„œλ²„μ™€ ν΄λΌμ΄μ–ΈνŠΈ κ΄€μ μ—μ„œ μ„€λͺ…ν•΄λ³΄μž. 이제 μ„œλ²„λŠ” μ„Έμ…˜ 정보λ₯Ό DB 에 μ €μž₯ν•˜μ§€ μ•Šκ³  μžμ‹ μ˜ Private Key 둜 μ•”ν˜Έν™” ν•΄ ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ μ„œλ²„μ—μ„œ κ΄€λ¦¬ν•˜λ˜ 인증 정보λ₯Ό λͺ¨λ‘ 기둝해 λ„˜κ²¨μ£Όμ–΄ ν΄λΌμ΄μ–ΈνŠΈκ°€ κ΄€λ¦¬ν•˜κΈ°λ‘œ ν–ˆλ‹€κ³  ν•˜μž. 이제 μΈκ°€λœ λ²”μœ„λΌλ˜κ°€ 유효 κΈ°κ°„μ΄λΌλ˜κ°€ ν•˜λŠ” μ„Έμ…˜μœΌλ‘œ κ΄€λ¦¬ν•˜λ˜ 정보듀이 ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ 전솑될 것이닀.

  1. ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ—κ²Œ 둜그인 μš”μ²­μ„ 보낸닀.
  2. μ„œλ²„λŠ” ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ μžμ‹ μ˜ Private Key 둜 μ•”ν˜Έν™” ν•œ 데이터λ₯Ό 쿠킀에 μ‹€μ–΄ 보낸닀.
  3. ν΄λΌμ΄μ–ΈνŠΈλŠ” 이미 μΈμ¦μ„œ λ°œκΈ‰ 기관을 톡해 μ˜¬λ°”λ₯Έ μ„œλ²„μž„μ„ 확인 ν–ˆμ§€λ§Œ, μ„œλ²„μ˜ Public Key λ₯Ό κ°–κ³  있기 λ•Œλ¬Έμ— 쀑간에 λ„€νŠΈμ›Œν¬ λ³€κ²½ λ“±μœΌλ‘œ μ„œλ²„μ˜ 접속 κ²½λ‘œκ°€ λ³€κ²½λ˜λ”λΌλ„ ν•΄λ‹Ή μ„œλ²„μ˜ 도메인이 보낸 응닡인지 검증이 κ°€λŠ₯ν•˜λ‹€.
  4. ν΄λΌμ΄μ–ΈνŠΈλŠ” 이제 μ„œλ²„μ™€ λΉ λ₯΄κ²Œ ν†΅μ‹ ν•˜κΈ° μœ„ν•΄ μ„œλ²„μ˜ Public Key 둜 μžμ‹ μ˜ PC κ°€ λ§Œλ“  λŒ€μΉ­ν‚€λ₯Ό μ•”ν˜Έν™” ν•΄ 쿠킀와 ν•¨κ»˜ μ „μ†‘ν•œλ‹€.

ν΄λΌμ΄μ–ΈνŠΈ μž…μž₯μ—μ„œλŠ” μ„œλ²„μ˜ Private Key 둜 데이터λ₯Ό μž…μΆ•ν–ˆκΈ° λ•Œλ¬Έμ— μ„œλ²„μ˜ 신원이 증λͺ…λ˜λŠ” 효과λ₯Ό κ°–λŠ”λ‹€.

ν΄λΌμ΄μ–ΈνŠΈλŠ” μ„œλ²„μ˜ Private Key λ₯Ό 가지고 μžˆμ§€ μ•ŠμœΌλ‹ˆ 토큰을 λ³€μ‘°ν•˜λŠ” 것은 쉽지 μ•Šλ‹€. ν•˜μ§€λ§Œ Public Key λ₯Ό 가지고 있기 λ•Œλ¬Έμ— μˆ˜μ—†μ΄ λ³€μ‘°ν•˜λ‹€ 보면 Public Key 둜 정상 ν•΄λ…λ˜λŠ” λ³€μ‘°λœ 토큰 데이터λ₯Ό 얻을 수 μžˆλ‹€. μ΄λ ‡κ²Œ μœ νš¨κΈ°κ°„μ΄ λŠ˜μ–΄λ‚˜κ±°λ‚˜ μΈκ°€λœ λ²”μœ„κ°€ λŠ˜μ–΄λ‚œ λ³€μ‘°λœ 토큰을 λ³΄λƒˆμ„ λ•Œ μ„œλ²„λŠ” ν† ν°λ§Œ ν™•μΈν•΄μ„œλŠ” λ³€μ‘° μ—¬λΆ€λ₯Ό μ•Œ 수 μ—†λ‹€. μ„œλ²„λŠ” μ„Έμ…˜κ³Ό 달리 토큰을 λ³΄κ΄€ν•˜κ³  μƒνƒœκ΄€λ¦¬λ₯Ό ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ΄λ‹€. κ²°κ΅­ μ„œλ²„λŠ” 이것이 λ³€μ‘°λ˜μ§€ μ•Šμ•˜μŒμ„ κ²€μ¦ν•˜κΈ° μœ„ν•΄μ„œ μ„Έμ…˜μ²˜λŸΌ DB 에 토큰 정보λ₯Ό κΈ°λ‘ν•˜κ±°λ‚˜, 토큰에 λ³€μ‘°ν•  수 μ—†λŠ” 무언가λ₯Ό μΆ”κ°€ν•΄μ•Όν•œλ‹€.

κ·Έλž˜μ„œ μΆ”κ°€λœ κΈ°λŠ₯이 μ„œλͺ…(Signature)이닀.

μ„œλͺ…을 μœ„ν•΄ Private Key 에 SHA-256, SHA-512 와 같은 ν•΄μ‹œ ν•¨μˆ˜μ™€ secret-keyλ₯Ό μΆ”κ°€ν•œλ‹€. 그러면 Private Key λŠ” 토큰을 생성할 λ•Œ ν•΄μ‹± κ²°κ³Όλ₯Ό μ„œλͺ…μœΌλ‘œ μΆ”κ°€ν•œλ‹€. ν•΄μ‹œ ν•¨μˆ˜λŠ” λ³΅ν˜Έν™”κ°€ λΆˆκ°€λŠ₯ν•˜κΈ° λ•Œλ¬Έμ— ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ λ³€μ‘°ν•˜λŠ” 것이 λΆˆκ°€λŠ₯ν•˜λ‹€. 이제 μ„œλ²„λŠ” ν΄λΌμ΄μ–ΈνŠΈκ°€ 보낸 토큰을 λ‹€μ‹œ ν•΄μ‹±ν•΄ μΆ”κ°€λœ μ„œλͺ…μ˜ 일치 μ—¬λΆ€λ₯Ό ν™•μΈν•˜λ©΄ ν† ν°μ˜ λ³€μ‘° 유무λ₯Ό 확인할 수 있게 λ˜λŠ” 것이닀.

즉, λ‹¨μˆœνžˆ λΉ„λŒ€μΉ­ν‚€λ‘œλ§Œ μ‚¬μš©ν•œλ‹€λ©΄ ν•œ 쌍이 λ˜λŠ” 두 개의 ν‚€λ₯Ό 생성 ν›„ λ°°ν¬λ˜λŠ” μˆœκ°„ Private/Public Key κ°€ λ˜μ–΄μ•Όν•˜μ§€λ§Œ, 이런 차이점으둜 인해 μ‹€μ œλ‘œλŠ” μƒμ„±ν•˜λŠ” μˆœκ°„ λΆ€ν„° Private/Public Key κ°€ 정해지고 Private Key 의 μš©λŸ‰μ΄ 더 큰 것이닀.

2. Token

이제 토큰이 μ‹€μ œλ‘œ μ–΄λ–»κ²Œ μž‘λ™ν•˜λŠ”μ§€λ₯Ό ν™•μΈν•΄λ³΄μž.

Token

  1. ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ— 둜그인 μš”μ²­.
  2. μ„œλ²„λŠ” λ‘œκ·ΈμΈμ„ 처리(authorization), μ„œλ²„μ˜ Private Key 둜 μƒμ„±ν•œ 인가/λ§Œλ£Œμ‹œκ°„ λ“± μžμ„Έν•œ 인증 정보가 λ‹΄κΈ΄ 토큰을 ν΄λΌμ΄μ–ΈνŠΈμ— λ°˜ν™˜(JWT, OAuth 의 ν˜•νƒœμ΄λ©° μ€‘μš”ν•œ 것은 이 κ³Όμ •μ—μ„œ authentication μ²˜λ¦¬κ°€ μ•„λ‹Œ authorization 처리λ₯Ό ν•œλ‹€λŠ” 것이닀).
  3. ν΄λΌμ΄μ–ΈνŠΈλŠ” 토큰과 ν•¨κ»˜ μ„œλ²„μ— ν•„μš”ν•œ μž‘μ—…μ„ μš”μ²­.
  4. μ„œλ²„λŠ” ν† ν°μ˜ μ„œλͺ…이 μœ νš¨ν•œμ§€ 검증. μ„œλͺ…이 μœ νš¨ν•˜λ‹€λ©΄ Public Key 둜 토큰에 기둝된 μžμ„Έν•œ 인증 정보λ₯Ό 확인해 토큰이 μœ νš¨ν•œμ§€ 확인 ν›„ μ‚¬μš©μž μš”μ²­μ„ μ²˜λ¦¬ν•΄ 응닡.

이제 μ„œλ²„λŠ” 맀번 Session 이 μ €μž₯된 DB 전체λ₯Ό μ‘°νšŒν•΄ κ²€μ¦ν•˜λŠ” λŒ€μ‹  μ‚¬μš©μžκ°€ λ³΄κ΄€ν•˜κ³  있던 ν† ν°μ˜ 유효 μ—¬λΆ€λ§Œ 확인 ν›„ μš”μ²­μ„ μ²˜λ¦¬ν•˜κΈ° λ•Œλ¬Έμ— μ„œλ²„μ˜ 뢀담이 쀄어든닀. ν΄λΌμ΄μ–ΈνŠΈλŠ” μ„œλ²„μ˜ λΉ„λ°€ν‚€λ‘œ μ•”ν˜Έν™”λœ 데이터λ₯Ό 톡해 μ„œλ²„μ˜ 신원을 ν™•μΈν•˜κ³  μ‹ λ’°ν•  수 있으며, μ„œλ²„λŠ” μžμ‹ μ˜ μ„œλͺ…을 μΆ”κ°€ν•΄ 토큰이 λ³€μ‘°λ˜μ§€ μ•Šμ•˜μŒμ„ ν™•μΈν•˜κ³  μ‹ λ’°ν•  수 있기 λ•Œλ¬Έμ΄λ‹€.

ν•˜μ§€λ§Œ 토큰에도 취약점은 μ‘΄μž¬ν•œλ‹€. μ„œλ²„μ—μ„œλŠ” 토큰에 κ΄€ν•œ μ–΄λ–€ 정보도 λ³΄κ΄€ν•˜κ³  κ΄€λ¦¬ν•˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ— 토큰 자체의 νƒˆμ·¨μ— μ·¨μ•½ν•˜λ‹€. κ·Έλ ‡κΈ° λ•Œλ¬Έμ— μ„œλ²„λŠ” 짧은 μ‹œκ°„μ„ κ°–λŠ” Access Token κ³Ό, 갱신에 μ‚¬μš©ν•  Refresh Token 을 λ°œν–‰ν•œλ‹€. ν˜Ήμ‹œλΌλ„ Access Token 이 μœ μΆœλ˜λ”λΌλ„ Refresh Token 이 μ—†λ‹€λ©΄ νƒˆμ·¨ν•œ 토큰을 μ§€μ†μ μœΌλ‘œ μ‚¬μš©ν•  수 없도둝 ν•˜κΈ° μœ„ν•¨μ΄λ‹€.

일반적으둜 Access Token 은 λΆ„~μ‹œκ°„ λ‹¨μœ„λ₯Ό κ°–κ³ , Refresh Token 은 일~μ£Ό λ‹¨μœ„λ₯Ό κ°–λŠ”λ‹€. 그리고 일반적으둜 λ‘œκ·ΈμΈν•˜λŠ” ν™˜κ²½μ— λ”°λΌμ„œλ„ ν† ν°μ˜ μœ νš¨μ‹œκ°„μ΄ λ‹€λ₯Έλ°, μ•±μ—μ„œ 둜그인 λ˜λŠ” 경우 μŠ€λ§ˆνŠΈν°μ€ 개인 기기이기 λ•Œλ¬Έμ— μœ νš¨μ‹œκ°„μ΄ 비ꡐ적 κΈ΄ 토큰이 λ°œν–‰λ˜λŠ” 반면, μ›Ήμ—μ„œ 둜그인 λ˜λŠ” 경우 곡용 기기일 수 있기 λ•Œλ¬Έμ— μœ νš¨μ‹œκ°„μ΄ 맀우 짧은 토큰이 λ°œν–‰λœλ‹€.

{
    method: "GET",
    headers: {
        "Authorization": "Bearer ${JWT_TOKEN}"
    }
}

일반적으둜 이런 ν˜•νƒœλ‘œ 해더에 Bearer prefix κ°€ ν¬ν•¨λœ 토큰을 μ‹€μ–΄ 보낸닀.


4. Session vs Token πŸ‘©β€πŸ’»

Criteria Session authentication method Token-based authentication method
authentication 정보 μ €μž₯ μ„œλ²„(일반적으둜 크기가 μž‘μ€ JSON 포맷) ν΄λΌμ΄μ–ΈνŠΈ(크기가 큰 토큰)
authorization 을 μœ„ν•΄ ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ— μ „μ†‘ν•˜λŠ” 것 μΏ ν‚€(Session ID κ°€ ν¬ν•¨λœ) 토큰
authorization 검증을 μœ„ν•΄ μ„œλ²„μ—μ„œ ν•˜λŠ” 것 μ„Έμ…˜ DB λ₯Ό μ‘°νšŒν•΄ μœ νš¨μ„± 검증 토큰을 λ³΅ν˜Έν™” ν•˜κ³  μ„œλ©μ„ 검증
μ„œλ²„ κ΄€λ¦¬μžκ°€ μ‚¬μš©μžμ˜ λ‘œκ·Έμ•„μ›ƒ, authentication 정보 λ³€κ²½κ³Ό 같은 λ³΄μ•ˆ μž‘μ—… κ°€λŠ₯ μ—¬λΆ€ κ°€λŠ₯(μ„Έμ…˜μ΄ μ„œλ²„μ— μ €μž₯) λΆˆκ°€λŠ₯(토큰이 ν΄λΌμ΄μ–ΈνŠΈ 기기에 μ €μž₯)
취약점 MITM, CSRF MITM, 토큰 νƒˆμ·¨, breaches of the secret key
μ„ ν˜Έν•˜λŠ” κ³³ User-to-server Server-to-server

일반적으둜 μ„Έμ…˜μ€ Application μ—μ„œ μ‚¬μš©λ˜κ³  토큰은 server-to-server ν†΅μ‹ μ—μ„œ μ„ ν˜Έλœλ‹€. κ·Έ μ΄μœ λŠ” 토큰은 μ„œλ²„κ°€ μ•„λ‹Œ ν΄λΌμ΄μ–ΈνŠΈμ—μ„œ κ΄€λ¦¬ν•˜κ³ , μ„œλ²„λŠ” λ³€μ‘° 여뢀와 μœ νš¨μ„±λ§Œ κ²€μ¦ν•˜κΈ° λ•Œλ¬Έμ— Limitation of Session 의 λͺ¨λ“  λ¬Έμ œμ μ—μ„œ ν•΄λ°©λœλ‹€. 특히 MSA 같은 ν™˜κ²½μ—μ„œ server-to-server ν†΅μ‹ μ—μ„œ ν΄λΌμ΄μ–ΈνŠΈμ˜ μƒνƒœλ₯Ό μœ μ§€ν•  ν•„μš” 없이 각 μš”μ²­μ„ λ…λ¦½μ μœΌλ‘œ μ²˜λ¦¬ν•  수 있으며(stateless), μ„œλ²„ 간에 토큰을 μ „μ†‘ν•˜κ³  κ²€μ¦λ§Œ ν•˜λ©΄ λ˜λ―€λ‘œ λ‹€μˆ˜μ˜ μ„œλ²„κ°€ μ‚¬μš©μžμ˜ authentication/authorization 을 μ‰½κ²Œ κ³΅μœ ν•  수 μžˆμ–΄ μ„œλ²„μ˜ ν™•μž₯μ„±κ³Ό μœ μ—°μ„±μ„ ν–₯상(scalability)μ‹œν‚€κΈ° λ•Œλ¬Έμ΄λ‹€.

μ—¬μ „νžˆ μ„Έμ…˜λ§Œ μ‚¬μš©ν•˜λŠ” 곳도 있으며, ν† ν°λ§Œ μ‚¬μš©ν•˜λŠ” 곳도 있고 μ„žμ–΄μ„œ μ‚¬μš©ν•˜λŠ” 곳도 μžˆλ‹€. 특히 금육ꢌ 같이 닀쀑 접속을 μž¬ν•œν•˜λŠ” κ²½μš°λŠ” μ„Έμ…˜μ΄ λ°˜λ“œμ‹œ ν•„μš”ν•˜λ‹€. 반면 접속 κΈ°κΈ°λ₯Ό μ œν•œν•  ν•„μš”κ°€ 크지 μ•Šμ€ 경우 λ©€ν‹° λ””λ°”μ΄μŠ€μ— λ”°λ₯Έ μ„Έμ…˜ 관리 뢀담이 λ”μš± 컀지기 λ•Œλ¬Έμ— μ„Έμ…˜ λŒ€μ‹  토큰을 μ‚¬μš©ν•˜λ©΄ μ„œλ²„μ˜ 뢀담을 크게 쀄일 수 μžˆλ‹€. λ¬Όλ‘ , 접속 κΈ°κΈ°λ₯Ό μ œν•œν•΄μ•Όν•΄μ„œ μ„Έμ…˜μ„ μ‚¬μš©ν•˜λŠ” κ²½μš°λ„ server-to-server 톡신은 토큰을 μ‚¬μš©ν•˜λ©΄ 톡신 λΉ„μš©μ„ 크게 쀄일 수 μžˆμ–΄ μ„Έμ…˜λ§Œ μ‚¬μš©ν•˜λŠ” 것보닀 νš¨μœ¨μ μ΄λ‹€.

λ„€νŠΈμ›Œν¬ ν™˜κ²½μ΄ 쒋지 λͺ»ν•˜λ©° λͺ¨λ°”일 기기인 경우 토큰이 μ„Έμ…˜λ³΄λ‹€ μš©λŸ‰μ΄ 컀 μ„±λŠ₯에 영ν–₯을 λ―ΈμΉ  수 μžˆλŠ” 맀우 νŠΉμˆ˜ν•œ μƒν™©λ§Œ μ•„λ‹ˆλΌλ©΄ 토큰이 κ°–λŠ” μž₯점이 크기 λ•Œλ¬Έμ— λ§Žμ€ μ„œλ²„κ°€ 토큰을 λ„μž…ν•˜λŠ” 것이닀.


5. Web Storage πŸ‘©β€πŸ’»

1. Web Storage API

λΈŒλΌμš°μ €κ°€ 데이터λ₯Ό μ €μž₯ν•˜λŠ” 방법은 크게 Web Storage API 와 IndexedDB API λ‘˜λ‘œ λ‚˜λ‰œλ‹€. Web Storage API λŠ” λΈŒλΌμš°μ €μ— μ €μž₯되며 λΈŒλΌμš°μ €μ— μ˜ν•΄ κ΄€λ¦¬λ˜λ©° λ‹€μŒκ³Ό 같은 νŠΉμ§•μ„ κ°–λŠ”λ‹€.

  • 도메인 λ‹¨μœ„λ‘œ μ €μž₯(쿠킀와 동일)
  • λΈŒλΌμš°μ €λ§ˆλ‹€ λ‹€λ₯΄μ§€λ§Œ 5~10MB λ₯Ό μ €μž₯ν•œλ‹€(쿠킀와 달리 μ‚¬μ΄νŠΈλ³„ κ°œλ³„ μš©λŸ‰μ΄ μ•„λ‹Œ λΈŒλΌμš°μ € μ „μ²΄μ˜ μš©λŸ‰μ΄λ‹€)
  • 영ꡬ μ €μž₯ κ°€λŠ₯(session λ‹¨μœ„ λ˜λŠ” 영ꡬ μ €μž₯)

쿠킀와 λΉ„κ΅ν•˜λ©΄ μ—„μ²­λ‚˜κ²Œ 큰 μš©λŸ‰μ„ μ €μž₯ν•  수 μžˆμ§€λ§Œ μ‚¬μ΄νŠΈλ³„λ‘œ μ£Όμ–΄μ§€λŠ” μš©λŸ‰μ€ μ•„λ‹Œλ°λ‹€ λ„‰λ„‰ν•œ μš©λŸ‰λ„ μ•„λ‹ˆλ‹€. ν•˜μ§€λ§Œ 쿠킀에 λΉ„ν•΄μ„œλŠ” κ½€ λ„‰λ„‰ν•œ μš©λŸ‰μ„ μ‚¬μš©ν•  수 μžˆμ„ 뿐 μ•„λ‹ˆλΌ μΏ ν‚€λŠ” λͺ¨λ“  λ„€νŠΈμ›Œν¬ μš”μ²­μ— ν¬ν•¨λ˜κΈ° λ•Œλ¬Έμ— 톡신에 λΆˆν•„μš”ν•œ λ°μ΄ν„°λŠ” Local Storage λ˜λŠ” Session Storage 에 μ €μž₯ν•˜λŠ” 것이 ꢌμž₯λœλ‹€.

Local Storage 와 Session Storage 의 μ‚¬μš©λ²•μ€ λ™μΌν•˜λ‹€. 단지 Session Storage λŠ” μ„Έμ…˜μ΄ 만료되면 μžλ™μœΌλ‘œ μ‚­μ œλ˜λŠ” 반면, Local Storage λŠ” λ³„λ„λ‘œ μ œκ±°ν•˜μ§€ μ•ŠλŠ” ν•œ 영ꡬ μ €μž₯이 κ°€λŠ₯ν•˜λ‹€.

μ‚¬μš© κ°€λŠ₯ν•œ μ£Όμš” λ©”μ„œλ“œλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  • getItem(key:): 데이터 쑰회
  • setItem(key:value:): 데이터 μΆ”κ°€
  • removeItem(key:): 데이터 제거
  • clear(): μŠ€ν† λ¦¬μ§€ μ΄ˆκΈ°ν™”

그리고 Web Storage 에 데이터λ₯Ό μ €μž₯ν•  λ•Œ λͺ¨λ“  λ°μ΄ν„°λŠ” JSON λ¬Έμžμ—΄λ‘œ λ³€ν™˜ν•΄ μ €μž₯ν•˜λ„λ‘ ν•΄μ•Όν•œλ‹€. 그렇지 μ•ŠμœΌλ©΄ μ €μž₯된 데이터λ₯Ό κ°€μ Έμ˜¬ λ•Œ, 원본 데이터가 κ°μ²΄μ˜€λŠ”μ§€λ„ ꡬ뢄해 λ‹€μ‹œ JavaScript κ°μ±„λ‘œ νŒŒμ‹±ν•˜κ±°λ‚˜ μ—λŸ¬κ°€ λ°œμƒν•  λ•Œλ§ˆλ‹€ μ²˜λ¦¬ν•˜λ„λ‘ μ˜ˆμ™Έμ²˜λ¦¬λ₯Ό ν•΄μ•Όν•˜κΈ° λ•Œλ¬Έμ΄λ‹€.

const setStorage = (storage) => (key, value) => storage.setItem(key, JSON.stringify(value));

export const setLocalStorage = setStorage(localStorage);
export const setSessionStorage = setStorage(sessionStorage);

const getStorage = (storage) => (key) => storage.getItem(key);

export const getLocalStorage = getStorage(localStorage);
export const getSessionStorage = getStorage(sessionStorage);

const removeStorage = (storage) => (key) => storage.removeItem(key);

export const removeLocalStorage = removeStorage(localStorage);
export const removeSessionStorage = removeStorage(sessionStorage);

const clearStorage = (storage) => () => storage.clear();

export const clearLocalStorage = clearStorage(localStorage);
export const clearSessionStorage = clearStorage(sessionStorage);

2. IndexedDB API

IndexedDB API λŠ” MSW(Mock Service Worker) 처럼 Web Worker λ₯Ό μ‚¬μš©ν•΄ 데이터λ₯Ό μ €μž₯ν•˜λŠ” API 둜 비동기 μ²˜λ¦¬λ˜μ–΄ μ„±λŠ₯에 영ν–₯을 λ―ΈμΉ˜μ§€ μ•ŠλŠ” Low Level Local Storage λ‹€. 이것은 μ•„μ΄ν°μœΌλ‘œ 치면 CoreData, SwiftData, Realm 이런 것과 같은데 RDBMS 도 μ•„λ‹Œλ°λ‹€ NoSQL 도 μ•„λ‹ˆμ§€λ§Œ κ·Έλž˜λ„ NoSQL μͺ½μ— 쑰금 더 가깝닀. Firebase Storage λ˜λŠ” Realm, MongoDB λ₯Ό 닀루듯 μŠ€ν† λ¦¬μ§€λ₯Ό 닀룬닀.

IndexedDB API λŠ” λ‹€μŒκ³Ό 같은 νŠΉμ§•μ„ κ°–λŠ”λ‹€.

  • 도메인 λ‹¨μœ„λ‘œ μ €μž₯
  • λΈŒλΌμš°μ €λ§ˆλ‹€ λ‹€λ₯΄μ§€λ§Œ 1TB λ””μŠ€ν¬ κΈ°μ€€μœΌλ‘œ 600GB κΉŒμ§€ ν™•μž₯ κ°€λŠ₯
  • 영ꡬ μ €μž₯ κ°€λŠ₯(persistent μ„€μ • κ°€λŠ₯)

기본적으둜 μ„Έμ…˜ λ‹¨μœ„λ‘œ μ €μž₯λ˜μ§€λŠ” μ•Šμ•„ μ„Έμ…˜μ΄ λŠκ²¨λ„ λ°μ΄ν„°λŠ” μœ νš¨ν•˜λ‹€. λ””μŠ€ν¬κ°€ λΆ€μ‘±ν•  경우 λΈŒλΌμš°μ €λ‚˜ μš΄μ˜μ²΄μ œμ— μ˜ν•΄ 정리될 수 μžˆμœΌλ‚˜ persistentλ₯Ό μ§€μ •ν•˜λ©΄ λΈŒλΌμš°μ €λ§ˆλ‹€ λ‹€λ₯΄μ§€λ§Œ 1GB κ°€λŸ‰ 영ꡬ μ €μž₯이 κ°€λŠ₯ν•˜λ‹€.

3. IndexedDB Examples

<form id="customerInputForm">
  <label for="userId">아이디:</label>
  <input type="text" id="userId" name="userId" />
  <label for="name">이름:</label>
  <input type="text" id="name" name="name" />
  <button type="submit" class="btn--green">μ €μž₯</button>
  <button type="reset">μƒˆλ‘œκ³ μΉ¨</button>
</form>
<ul id="customerList"></ul>
input {
  outline: none;
  border: none;
  padding: 1px 0;
}

#customerInputForm,
#customerList {
  margin: 10px 0;
  padding: 0;
  color: black;
  font-weight: 700;
}

#customerInputForm,
#customerList li {
  height: 40px;
  padding: 10px;
  margin: 10px 0;
  background-color: #8aadc1;
  box-sizing: border-box;
  border: none;
  border-radius: 15px;
  display: flex;
  align-items: center;
}

#customerList li span {
  width: 70px;
}

#customerInputForm input,
#customerList input {
  width: 150px;
  margin: 0 10px 0 5px;
  user-select: none;
}

#customerList li input {
  background-color: #bcbcbc;
}

#customerList li.edit input {
  background-color: #fff;
}

#customerInputForm button,
#customerList button {
  margin-right: 5px;
  padding: 3px 15px;
  border: none;
  border-radius: 4px;
  font-weight: 700;
}

#customerList.edit button {
  display: none;
}
#customerList li.edit button {
  display: block;
}
#customerList li button.hidden {
  display: none;
}
button.btn--red {
  background-color: #ff3b3b;
}
button.btn--green {
  background-color: #20e62a;
}
const customerForm = document.querySelector('#customerInputForm');
const customerList = document.querySelector('#customerList');

const DATABASE_NAME = 'customer-database';
const TABLE_NAME = 'customer';

let db;
const actions = createActions();

const DBOpenRequest = indexedDB.open(DATABASE_NAME);

DBOpenRequest.onerror = (event) => {
  customerList.innerHTML = `<li>Error loading database.</li>`;
  console.error(event.target.error);
};

// 버전 μ—…κ·Έλ ˆμ΄λ“œκ°€ ν•„μš”ν•˜κ±°λ‚˜ 졜초 μƒμ„±μ‹œ μˆ˜ν–‰λ˜λŠ” μ½”λ“œλ‘œ ν…Œμ΄λΈ”, 컬럼과 같은 λ°μ΄ν„°λ² μ΄μŠ€ ꡬ쑰λ₯Ό μ •μ˜ν•œλ‹€.
DBOpenRequest.onupgradeneeded = (event) => {
  db = event.target.result;

  // μŠ€ν† μ–΄ 생성(ν…Œμ΄λΈ” 생성에 ν•΄λ‹Ή)
  // index λ₯Ό μƒμ„±ν•΄μ„œ κ²€μƒ‰μœΌλ‘œ μ‚¬μš©ν•  μˆ˜λŠ” μ—†λ‹€. 즉, autoIncrement λ₯Ό μ‚¬μš©ν•΄ 검색은 ν•  수 μ—†κ³ ,
  // μŠ€ν† μ–΄λ₯Ό κ΄€λ¦¬ν•˜κΈ° μœ„ν•œ primaryKey λ˜λŠ” key둜 openCursor μ—μ„œ 검색 결과에 λ‚˜μ˜€λŠ”
  // primaryKey λ˜λŠ” key 의 값이 value 에 λ“±λ‘λœ κ°’κ³Ό λ™μΌν•˜λ‹€.
  const objectStore = db.createObjectStore(TABLE_NAME, {
    keyPath: 'id',
    autoIncrement: true,
  });

  // 인덱슀 생성(컬럼 생성에 ν•΄λ‹Ή)
  objectStore.createIndex('userId', 'userId', { unique: true });
  objectStore.createIndex('name', 'name', { unique: false });
  customerList.innerHTML += '<li>Object store created.</li>';
};

DBOpenRequest.onsuccess = (event) => {
  customerList.innerHTML += '<li>Database initialised.</li>';
  db = DBOpenRequest.result; // equal to 'event.target.result'
  actions.load();
};

function createActions() {
  let [prevUserId, prevUserName] = [null, null];

  function idValidation(userId) {
    if (userId.trim().length < 5) {
      alert('μ•„μ΄λ””λŠ” 5자 이상 μž…λ ₯ν•΄μ£Όμ„Έμš”');
      return false;
    }
    return true;
  }

  function addCustomer() {
    const customer = {
      userId: customerForm[0].value,
      name: customerForm[1].value,
    };
    if (!idValidation(customer.userId)) return;
    // νŠΈλžœμž­μ…˜μ„ μ—΄ μŠ€ν† μ–΄ 이름 λ°°μ—΄, νŠΈλžœμž­μ…˜ λͺ¨λ“œλ₯Ό 지정
    const transaction = db.transaction([TABLE_NAME], 'readwrite');
    // νŠΈλžœμž­μ…˜μ— μ—΄λ¦° μŠ€ν† μ–΄ λ°°μ—΄ 쀑 단일 μŠ€ν† μ–΄ μ ‘κ·Ό
    const objectStore = transaction.objectStore(TABLE_NAME);
    // μš”μ²­ 처리
    objectStoreRequest = objectStore.add(customer);
    objectStoreRequest.onsuccess = (event) => {
      customerForm.reset();
    };

    objectStoreRequest.onerror = (event) => {
      debugger;
      let message =
          event.target.error.code === 0
              ? 'μ•„μ΄λ””λŠ” 쀑볡될 수 μ—†μŠ΅λ‹ˆλ‹€'
              : 'μ €μž₯을 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€';
      alert(message);

      console.error({
        Code: event.target.error.code,
        Name: event.target.error.name,
        Message: event.target.error.message,
      });
    };
  }

  function loadCustomers() {
    customerList.innerHTML = '';

    const objectStore = db.transaction(TABLE_NAME).objectStore(TABLE_NAME);
    const request = objectStore.openCursor();
    request.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor === null) return;
      // μŠ€ν† μ–΄μ— 'Key'κ°€ ν•˜λ‚˜μ΄λ―€λ‘œ cursor.primaryKey, cursor.key, value.id 3κ°œλŠ” λͺ¨λ‘ λ™μΌν•œ 값이닀.
      const { id, userId, name } = cursor.value;
      customerList.innerHTML += `
      <li>
        <span data-id="${id}">ID: ${id}</span>
        아이디: <input type="text" value="${userId}" readonly />
        이름: <input type="text" value="${name}" readonly />
        <button type="button" data-type="editMode">μˆ˜μ •</button>
        <button type="button" data-type="delete" class="btn--red">μ‚­μ œ</button>
        <button type="button" data-type="cancel" class="hidden">μ·¨μ†Œ</button>
        <button type="button" data-type="edit" class="btn--green hidden">μ €μž₯</button>
      </li>
      `;

      // Move on to the next cursor item
      cursor.continue();
    };

    request.onerror = (event) => {
      customerList.innerHTML = '<li>Store access error</li>';
      console.error(event.target.error);
    };
  }

  function toggleEditMode({ parentEl }) {
    const [, userId, name, editModeBtn, deleteBtn, cancelBtn, editBtn] = [
      ...parentEl.children,
    ];
    [customerList, parentEl].forEach((el) => el.classList.toggle('edit'));
    userId.readOnly = !userId.readOnly;
    name.readOnly = !name.readOnly;
    [editModeBtn, deleteBtn, cancelBtn, editBtn].forEach((el) =>
        el.classList.toggle('hidden')
    );
  }

  function openEditMode({ parentEl }) {
    const [, userId, name] = [...parentEl.children];
    prevUserId = userId.value;
    prevUserName = name.value ?? null;
    toggleEditMode({ parentEl });
  }

  function cancelEditMode({ parentEl }) {
    const [, userId, name] = [...parentEl.children];
    userId.value = prevUserId && prevUserId;
    name.value = prevUserName && prevUserName;
    [prevUserId, prevUserName] = [null, null];
    toggleEditMode({ parentEl });
  }

  function editCustomer({ id, userId, name }) {
    if (!idValidation(userId)) return;
    const objectStore = db
        .transaction(TABLE_NAME, 'readwrite')
        .objectStore(TABLE_NAME);
    const updateRequest = objectStore.put({ id, userId, name });
    updateRequest.onsuccess = (event) => {
      loadCustomers();
      customerList.classList.toggle('edit');
    };
  }

  function deleteCustomer({ id }) {
    const objectStore = db
        .transaction(TABLE_NAME, 'readwrite')
        .objectStore(TABLE_NAME);
    const deleteRequest = objectStore.delete(id); // primaryKey 둜 μ‚­μ œ
    deleteRequest.onsuccess = (event) => loadCustomers();

    // μ•„μ΄ν…œ νŠΉμ • κ°’μœΌλ‘œ μ‚­μ œν•˜κ³  싢을 경우 μŠ€ν† μ–΄λ‘œλΆ€ν„° index λ₯Ό μƒμ„±ν•˜κ³  값을 검색해
    // κ²€μƒ‰λœ μ•„μ΄ν…œμœΌλ‘œλΆ€ν„° primaryKey λ₯Ό 가져와 μ‚­μ œ.
    /*
    const nameRequest = objectStore.index('name').openCursor(name);
    nameRequest.onsuccess = (event) => {
      const cursor = event.target.result;
      if (cursor ===null) return;
      objectStore.delete(cursor.primaryKey)
      // cursor.continue();  // 볡수 μ‚­μ œμΌ 경우
    };
    */
  }

  return {
    add: addCustomer,
    load: loadCustomers,
    editMode: ({ parentEl }) => openEditMode({ parentEl }),
    edit: ({ id, userId, name }) => editCustomer({ id, userId, name }),
    cancel: ({ parentEl }) => cancelEditMode({ parentEl }),
    delete: ({ id }) => deleteCustomer({ id }),
  };
}

customerForm.addEventListener('submit', (event) => {
  event.preventDefault();
  actions.add();
});
customerForm.addEventListener('reset', actions.load);

customerList.addEventListener('click', (event) => {
  event.stopPropagation();
  if (event.target.tagName !== 'BUTTON') return;
  const li = event.target.parentElement;
  const [id, userId, name, parentEl] = [
    parseInt(li.firstElementChild.dataset.id),
    li.children[1].value,
    li.children[2].value,
    li,
  ];
  const type = event.target.dataset.type;
  actions[type]({ id, userId, name, parentEl });
});

transaction λ©”μ„œλ“œλŠ” string 으둜 단일 ν…Œμ΄λΈ” λ˜λŠ” λ°°μ—΄λ‘œ μ—¬λŸ¬ ν…Œμ΄λΈ”μ„ ν•œ λ²ˆμ— νŠΈλž™μž­μ…˜μœΌλ‘œ μ—΄ 수 μžˆμœΌλ‚˜, objectStore λ©”μ„œλ“œλŠ” μ—΄λ¦° νŠΈλž™μž­μ…˜ μ€‘μ—μ„œ 단일 ν…Œμ΄λΈ”μ„ μ—΄μ–΄μ„œ μž‘μ—…ν•œλ‹€.

μŠ€ν† μ–΄λ₯Ό μ—° ν›„ CRUD 에 μ‚¬μš©ν•˜λŠ” λ©”μ„œλ“œλŠ” λ‹€μŒκ³Ό κ°™λ‹€.

  • C: add λ©”μ„œλ“œ
  • R: cursor λ˜λŠ” index λ©”μ„œλ“œλ‘œ 인덱슀λ₯Ό 생성 ν›„ getμ΄λ‚˜ getAll λ©”μ„œλ“œ μ ‘κ·Ό
  • U: put λ©”μ„œλ“œ
  • D: delete λ©”μ„œλ“œ

storeλ₯Ό 생성할 λ•Œ λ°˜λ“œμ‹œ keyλ₯Ό ν•˜λ‚˜ μ§€μ •ν•΄μ•Όν•˜κ³  이것이 ν•˜λ‚˜μ΄λ©΄ 기본적으둜 primaryKey둜 μ‚¬μš©λœλ‹€. ν•˜μ§€λ§Œ 이 Key λŠ” λ°μ΄ν„°μ˜ Update, Delete 와 같은 μž‘μ—…μ„ ν•  λ•Œ ν•„μš”ν•œ κ°’μœΌλ‘œ, autoIncrement 둜 μ„€μ •ν•œ 경우 직접 μ»¬λŸΌμ— ν•΄λ‹Ήν•˜λŠ” indexλ₯Ό μƒμ„±ν•œ 것이 μ•„λ‹ˆκΈ° λ•Œλ¬Έμ— indexλ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•  수 μ—†μŒμ— μ£Όμ˜ν•œλ‹€.




    Reference

    1. λ°•μ˜μ›…, β€œν”„λ‘ νŠΈμ—”λ“œ μ›Ή 개발의 λͺ¨λ“  것 초격차 νŒ¨ν‚€μ§€ Online.” fastcampus.co.kr. last modified unknown, Fast Campus.
    2. β€œUsing HTTP cookies.” MDN Web Docs. Feb. 23, 2024, accessed Apr. 18, 2024, MDN - HTTP cookies.
    3. β€œSession vs Token Based Authentication.” GeeksforGeeks. Jul. 4, 2022, accessed Apr. 18, 2024, Session vs Token Based Authentication.