로그인 기능의 핵심: 데이터베이스
로그인 기능 동작 원리
- 사용자가 아이디와 비밀번호를 입력하고 `로그인` 버튼을 클릭.
- 서버는 사용자가 입력한 아이디를 데이터베이스에서 검색.
- 해당 아이디가 존재하면, 저장된 비밀번호와 사용자가 입력한 비밀번호가 일치하는지 비교.
- 모두 일치하면 `로그인 성공`, 그렇지 않으면 `로그인 실패`를 응답.
로그인 기능에는 사용자 정보를 저장하고 조회할 수 있는 데이터베이스가 반드시 필요함.
1. 사용자 테이블 구조 설계
사용자 정보를 저장할 `users` 테이블을 설계. 로그인 기능에 필수적인 정보는 아이디와 비밀번호. 사용자를 구분할 고유 번호 (id)를 추가 -> 테이블 구성.
`users` 테이블 구조
| 칼럼 이름 | 자료형 | 제약 조건 | 설명 |
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 사용자 고유 번호 (자동 증가) |
| username | TEXT | NOU NULL, UNIQUE | 사용자 아이디 (필수 입력, 중복 불가) |
| password | TEXT | NOT NULL | 사용자 비밀번호 (필수 입력) |
- UNIQUE : `username` 칼럼에 추가된 제약 조건. 이메일이나 아이디처럼, 다른 사용자와 절대 중복되어서는 안 되는 값에 설정.
실제로 비밀번호를 저장할 때는 `해싱(Hashing)`이라는 암호화 과정을 거쳐, 원래 값을 알아볼 수 없는 형태로 변환하여 저장해야 함. (예: `bcrypt` 패키지 사용)
2. 프론트엔드와 백엔드 코드 분리 설계
- public 폴더 : 브라우저가 직접 접근하여 화면에 그리는 파일들(HTML, 프론트엔드, JS, CSS 등)을 담음.
- server.js : 데잍터베이스를 제어하고, 프론트엔드의 요청에 응답하는 API 서버의 역할을 함.
3. 로그인 상태 유지 설계 (feat. JWT)
HTTP 통신은 기본적으로 `상태가 없는 (Stateless)` 특성을 가짐. 즉, 서버는 방금 전의 요청을 기억하지 못함. 그래서 로그인 한 후 다른 페이지로 이동하면, 서버는 우리가 로그인했다는 사실을 잊어버림. 이것을 방지하기 위해 JWT 사용.
자유이용권: JWT (Json Web Token)
서버는 사용자의 고유 정보 (예: id)를 담아 암호화된 토큰(Token)을 생성하여 사용자에게 전달. 이 토큰은 비밀키로 서명되어 있어 위조가 불가능.
로그인 상태 유지 흐름
- 로그인 요청 : 사용자가 `login.html` 에서 아이디/비밀번호를 입력, 프론트엔드 JS는 이 정보를 서버의 `api/login` 으로 보냄.
- 서버의 인증 및 토큰 발급 : 서버는 DB를 확인하여 정보가 일치하면, 해당 사용자의 정보를 담은 JWT(자유이용권)를 생성 프론트엔드에 응답으로 보내줌.
- 토큰 저장 : 프론트엔드 JS는 서버로부터 받은 JWT를 브라우저의 안전한 저장 공간 ( localStorage )에 저장.
- 인증이 필요한 요청 : 사용자가 로그인 후에만 접근 가능한 페이지 ( 예: `main.html` 의 사용자 정보 ) 를 요청할 때, 프론트엔드 JS는 저장해 둔 JWT를 요청 헤더 (Header)에 포함하여 서버로 보냄.
- 서버의 인가 : 서버는 요청에 포함된 JWT가 유효한지 (위조되지 않았는지) 검증하고, 유효한 경우에만 요청을 허락해 데이터를 보내줌. 유효하지 않으면 "로그인이 필요합니다" 와 같은 에러를 응답함.
4. 페이지 서빙
node.js 서버가 사용자가 접속했을 때 눈에 보이는 화면, 즉 프론트엔드 파일들(login.html, main.html 등)을 보내주는 것.
http 모듈로 서버를 만들고, fs 모듈로 요청된 URL에 해당하는 파일을 읽어 브라우저에 전송.
5. 로그인 APi (`POST/api/login`)
로그인 API는 프론트 엔드로부터 사용자가 입력한 아이디와 비밀번호를 받아, 데이터베이스에 저장된 정보와 일치하는지 검증하는 핵심적인 역할을 함.
로그인 처리 흐름
- 프론트엔드가 fetch 를 이용해 /api/login 경로로 POST 요청을 보냄. 이때 요청 본문(body)에 { username: `...`, password: `...` } 형태의 JSON 데이터를 전달.
- 서버는 요청 본문(body)의 JSON 데이터를 파싱하여 아이디와 비밀번호를 추출.
- SELECT SQL 문을 사용, 데이터베이스의 users 테이블에서 프론트엔드가 보낸 아이디(username)와 일치하는 사용자를 찾음.
- [분기 처리]
- 사용자를 찾지 못한 경우 : "존재하지 않는 아이디입니다." 와 같은 실패 메시지를 응답.
- 사용자를 찾은 경우 : 데이터베이스에 저장된 비밀번호와 프론트엔드가 보낸 비밀번호가 일치하는지 비교.
5. [비밀번호 비교 결과]
- 비밀번호가 일치하지 않는 경우 : "비밀번호가 틀렸습니다." 와 같은 실패 메시지를 응답.
- 비밀번호가 일치하는 경우 : 로그인 성공! 다음 단계인 JWT를 발급하여 성공 응답과 함께 프론트엔드로 보내줌.
JWT 처리
JWT 패키지 설치 : npm install jsonwebtoken
JWT 생성 (토큰 발급)
jwt.sign() 함수를 사용하여 토큰을 생성.
JWT 검증 (자유이용권 확인)
로그인 후, `마이페이지`처럼 인증된 사용자만 접근할 수 있는 API를 요청할 때, 클라이언트는 발급받은 JWT를 요청 헤더(Header)에 담아 보냄. 서버는 이 토큰이 유효한지 검증해야 함.
6. JS 코드 (`public/app.js`)
하나의 app.js 파일이 현재 어떤 HTML 페이지에 있는지 스스로 판단, 각 페이지에 맞는 기능을 수행하도록 코드를 작성.
브라우저 저장소: `localStorage`
서버로부터 받은 JWT 토큰은 브라우저를 껐다 켜도 유지되어야 함.
localStorage는 이러한 데이터를 브라우저에 영구적으로 저장할 수 있게 해주는 간단한 저장 공간.
- 저장 : localStorage.setItem('token', 'JWT_TOKEN_HERE')
- 읽기 : localStorage.getItem('token')
- 삭제 : localStorage.removeItem('token')
프로젝트 파일 구조
login-app/
├── public/
│ ├── login.html
│ ├── signup.html
│ ├── main.html
│ └── app.js
├── users.db
└── server.js
Full-Stack 핵심 개념 가이드
서버와 클라이언트가 소통하는 방식 (HTTP 매서드, CORS) 과 프론트엔드 코드 구성의 원리.
1. API 요청/처리시 사용되는 method
우리가 `fetch` 를 사용해 서버에 요청을 보낼 때, method: 'POST' 와 같이 '메서드(method)'를 지정. HTTP 요청 메서드는 클라이언트 (브라우저)가 서버에게 요청의 목적이나 의도가 무엇인지 알려주는 '행동 동사'와 같음.
데이터베이스의 CRUD (Create, Read, Update, Delete) 작업은 각각의 HTTP 메서드와 관례적으로 연결됨.
| HTTP 메서드 | CRUD 역할 | 설명 |
| GET | Read (읽기) | 서버로부터 데이터를 조회할 때 사용. (예: 게시물 목록 보기) |
| POST | Create (생성) | 서버에 새로운 데이터를 생성할 때 사용. (예: 회원가입, 새 글 작성) |
| PUT | Update (수정) | 기존 데이터를 전체적으로 수정할 때 사용. (예: 회원 정보 전체 수정) |
| DELETE | Delete (삭제) | 기존 데이터를 삭제할 때 사용. (예: 게시물 삭제) |
2. CORS
CORS (Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 웹 브라우저에서 실행되는 스크립트가 다른 출처 (Origin)의 리소스에 접근할 수 있도록 허용하는 보안 메커니즘
이것을 이해하려면 `동일 출처 정책(Same-Origin Policy)` 을 알아야 함. 이 정책은 "A라는 웹사이트에서 실행된 스크립트는 A 웹사이트의 리소스만 접근할 수 있다"는 기본적인 웹 보안 규칙. (예: "우리 집 안에서는 우리 집 물건만 쓸 수 있다")
'출처(Origin)' : URL의 프로토콜(http), 호스트(localhost), 포트(:3000) 세 가지를 조합한 것.
CORS는 다른 출처의 서버가 "내 데이터는 저 웹사이트에서 사용해도 좋아" 라고 허락해 주는 '허가증'과 같은 역할을 함.
3. CORS Preflight 요청과 헤더 설정
CORS Preflight 요청 (`OPTIONS` 메서드)
우리가 만든 프론트엔드 코드가 Content-Type: 'application/json' 과 같은 특별한 헤더를 담아 서버에 `POST` 요청을 보내려고 할 때, 브라우저는 본 요청을 보내기 전에 예비 요청(Preflight Request)을 보냄.
이 예비 요청은 OPTIONS 라는 HTTP 메서드를 사용, 서버가 이 요청에 대해 POST 메서드 허용, Content-Type 헤더도 허용 하는 응답을 보내주면 그제서야 원래 보내려던 `POST` 요청을 보냄.
CORS 헤더 설정
Access-Control-Allow-Origin : 어떤 출처의 요청을 허용할지 지정. (예 : ' * ' 전부 허용)
Access-Control-Allow-Methods : 허용할 HTTP 메서드 목록을 지정. (예 : 'POST, GET, PUT, DELETE, OPTIONS')
Access-Control-Allow-Headers : 허용할 요청 헤더 목록을 지정. (예 : 'Content-Type, Authorization')
4. `app.js` 파일 하나로 여러 페이지를 제어하는 원리
"현재 페이지에 해당 요소가 존재하는지 먼저 확인하기"
JavaScript에서 document.querySelector("#존재하지_않는_id")를 실행하면, 에러가 발생하는 대신 null 값이 반환됩니다. if문에서 null은 false로 취급.
이 원리를 이용해 다음과 같이 코드를 구성할 수 있음.
document.addEventListener('DOMContentLoaded', () => {
// 1. 각 페이지에 있을 법한 요소들을 일단 모두 선택해 본다.
const loginForm = document.querySelector("#login-form");
const signupForm = document.querySelector("#signup-form");
const welcomeMessage = document.querySelector("#welcome-message");
// 2. 각 요소의 존재 여부를 if문으로 확인하여 로직을 분기한다.
// loginForm 변수에 요소가 담겨있다면 (null이 아니라면),
// 현재 페이지는 login.html이라고 판단할 수 있다.
if (loginForm) {
// 이 안의 코드는 login.html에서만 실행됩니다.
console.log("여기는 로그인 페이지입니다.");
loginForm.addEventListener('submit', (e) => {
// ... 로그인 관련 로직 ...
});
}
// signupForm 변수에 요소가 담겨있다면,
// 현재 페이지는 signup.html이라고 판단할 수 있다.
if (signupForm) {
// 이 안의 코드는 signup.html에서만 실행됩니다.
console.log("여기는 회원가입 페이지입니다.");
// ... 회원가입 관련 로직 ...
}
// ... main.html에 대한 로직도 마찬가지 ...
});
이러한 '특징 감지(Feature Detection)' 방식을 통해, 우리는 각 페이지마다 별도의 JS 파일을 만들 필요 없이 하나의 파일로 여러 페이지의 기능을 효율적으로 관리할 수 있음.
'DB' 카테고리의 다른 글
| SQL 사용법 (0) | 2025.12.10 |
|---|---|
| 데이터 베이스 구조 (0) | 2025.12.10 |
| 쿼리문 명령어 (0) | 2025.12.09 |
| MySQL 설치방법 (0) | 2025.12.09 |
| 기본 개념 잡기 (0) | 2025.11.27 |