주소는 서버 안의 접수 창구다
지난 장에서 우리는 서버가 요청을 받고 응답을 보내는 흐름을 살펴보았습니다. 이제 한 단계 더 나아가 보겠습니다. 실제 서버는 한 가지 일만 하지 않습니다.
작은 문의 접수 서버를 떠올려 보겠습니다.
첫 화면을 보여 주는 창구
회사 소개를 보여 주는 창구
상품 목록을 보여 주는 창구
문의를 제출받는 창구
특정 상품 하나를 보여 주는 창구
이렇게 서버 안에 여러 접수 창구를 만드는 작업을 라우팅이라고 합니다. Express 공식 문서에서도 라우팅을 "애플리케이션의 끝점이 클라이언트 요청에 어떻게 응답할지 정하는 일"로 설명합니다.1 여기서 끝점이라는 말은 어렵게 느껴질 수 있지만, 지금은 "서버 안의 특정 주소 창구"라고 이해하면 충분합니다.
Express에서는 보통 다음 모양으로 라우트를 만듭니다.
app.get('/주소', (req, res) => {
res.send('응답 내용');
});이 코드는 이렇게 읽으면 됩니다.
"GET 방식으로
/주소에 요청이 오면, 이 함수를 실행해서 응답을 보낸다."
여기서 중요한 것은 두 가지입니다.
HTTP 메서드: GET, POST처럼 요청의 성격을 나타내는 말
URL 경로:
/,/about,/products처럼 서버 안의 창구 이름
즉, 라우팅은 "요청 방식 + 주소"를 보고 어떤 일을 할지 정하는 일입니다.
AI 생성 이미지
이번에 만들 것
이번 장에서는 하나의 Express 서버 안에 여러 주소를 만들겠습니다.
우리가 만들 주소는 다음과 같습니다.
| 요청 방식 | 주소 | 하는 일 |
|---|---|---|
| GET | / | 첫 화면 문장 보여 주기 |
| GET | /about | 소개 문장 보여 주기 |
| GET | /products | 상품 목록 보여 주기 |
| GET | /products/:id | 특정 상품 번호 읽기 |
| GET | /search?q=coffee&page=2 | 검색어와 페이지 번호 읽기 |
| POST | /contact | 문의 제출을 받았다고 응답하기 |
아직 데이터베이스에 저장하거나 실제 HTML 화면을 만들지는 않습니다. 이번 장의 목표는 "주소마다 다른 일이 실행되는 감각"을 익히는 것입니다.
app.js 파일 열기
VS Code에서 지난 장에 만든 express-practice 폴더를 엽니다. 그리고 app.js 파일을 엽니다.
기존 코드가 조금 달라도 괜찮습니다. 핵심 구조는 다음과 비슷할 것입니다.
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('안녕하세요. 첫 Express 서버입니다.');
});
app.listen(3000, () => {
console.log('서버가 http://localhost:3000 에서 실행 중입니다.');
});위 코드에서 app.listen()은 서버를 켜는 코드입니다. 라우트는 보통 app.listen()보다 위에 적습니다.
이제 app.get('/') 아래에 새 주소들을 하나씩 추가해 보겠습니다.
첫 화면과 소개 페이지 나누기
먼저 기본 주소 /의 응답을 조금 바꾸겠습니다.
app.get('/', (req, res) => {
res.send('문의 접수 서버의 첫 화면입니다.');
});/는 가장 기본이 되는 주소입니다. 브라우저에서 http://localhost:3000으로 접속하면 이 라우트가 실행됩니다.
이제 소개 페이지 주소를 추가합니다.
app.get('/about', (req, res) => {
res.send('이 서버는 문의와 상품 정보를 연습하기 위한 서버입니다.');
});이 코드는 이렇게 읽습니다.
"GET 방식으로
/about주소에 요청이 오면 소개 문장을 보내라."
여기서 GET은 정보를 가져오는 요청에 자주 사용합니다. 식당에 비유하면 "메뉴판을 보여 주세요"에 가깝습니다. 서버의 내용을 바꾸기보다, 이미 있는 정보를 조회하는 느낌입니다.
이제 터미널에서 서버를 실행합니다.
node app.js성공하면 터미널에 다음과 비슷한 문장이 보입니다.
서버가 http://localhost:3000 에서 실행 중입니다.브라우저에서 아래 두 주소를 차례대로 열어 보세요.
http://localhost:3000
http://localhost:3000/about첫 번째 주소에서는 첫 화면 문장이 보이고, 두 번째 주소에서는 소개 문장이 보이면 성공입니다. 같은 서버인데 주소에 따라 다른 응답이 나온 것입니다.
코드를 수정했는데 브라우저 내용이 바뀌지 않는다면 서버를 한 번 껐다가 다시 켜 주세요. 터미널에서 Ctrl + C를 누른 뒤 다시 실행하면 됩니다.
node app.jsGET은 조회, POST는 제출
HTTP 메서드는 요청의 성격을 나타냅니다. 처음에는 두 가지만 기억하면 충분합니다.
| 메서드 | 생활 속 느낌 | 자주 쓰는 상황 |
|---|---|---|
| GET | 보여 주세요 | 목록 보기, 상세 보기, 검색하기 |
| POST | 제출할게요 | 문의 보내기, 예약 신청하기, 글 작성하기 |
GET은 정보를 가져올 때 많이 씁니다.
예를 들어 다음 요청은 "상품 목록을 보여 주세요"라는 뜻에 가깝습니다.
app.get('/products', (req, res) => {
res.send('상품 목록입니다: 커피, 머그컵, 노트');
});브라우저에서 아래 주소를 열면 확인할 수 있습니다.
http://localhost:3000/products성공하면 다음 문장이 보입니다.
상품 목록입니다: 커피, 머그컵, 노트반면 POST는 사용자가 무언가를 서버에 제출할 때 많이 씁니다. 문의 폼을 보내거나 예약 신청서를 제출하는 상황을 떠올리면 됩니다.
app.get('/products') 아래에 다음 코드를 추가합니다.
app.post('/contact', (req, res) => {
res.send('문의가 제출되었습니다.');
});이 코드는 "POST 방식으로 /contact에 요청이 오면 문의가 제출되었다고 응답하라"는 뜻입니다.
주의할 점이 있습니다. 브라우저 주소창에 주소를 입력해서 접속하면 보통 GET 요청이 갑니다. 그래서 http://localhost:3000/contact를 주소창에 입력해도 이 POST 라우트가 바로 실행되지 않습니다. 이때 브라우저에 Cannot GET /contact처럼 보일 수 있는데, 서버가 고장 난 것이 아니라 "GET 방식의 /contact 창구는 아직 만들지 않았다"는 뜻에 가깝습니다.
POST 요청은 터미널에서 간단히 확인할 수 있습니다. 다만 서버가 켜져 있는 터미널은 그대로 두어야 합니다. 새 터미널 창이나 새 터미널 탭을 하나 더 열고, 거기에서 아래 명령을 입력해 주세요.
macOS, Linux, Git Bash에서는 다음 명령을 입력합니다.
curl -X POST http://localhost:3000/contactWindows PowerShell을 사용한다면 다음 명령을 입력합니다.
Invoke-RestMethod -Method Post http://localhost:3000/contact성공하면 터미널에 다음 응답이 보입니다.
문의가 제출되었습니다.이 실습에서 중요한 점은 /contact라는 주소만이 아닙니다. 같은 주소라도 GET인지 POST인지에 따라 다른 창구가 될 수 있다는 점입니다. Express의 app.get()과 app.post()는 각각 GET 요청과 POST 요청을 처리하는 라우트를 만듭니다.1
주소 안의 값을 꺼내는 라우트 파라미터
이번에는 특정 상품 하나를 보여 주는 주소를 만들어 보겠습니다.
상품 목록 주소는 이렇게 만들었습니다.
/products그렇다면 1번 상품, 2번 상품, 3번 상품은 어떻게 표현할 수 있을까요?
/products/1
/products/2
/products/3이런 주소를 하나씩 모두 만들 수도 있습니다.
app.get('/products/1', ...)
app.get('/products/2', ...)
app.get('/products/3', ...)하지만 상품이 100개가 되면 주소도 100개를 직접 써야 합니다. 좋은 방법이 아닙니다.
Express에서는 변하는 부분에 이름표를 붙일 수 있습니다.
app.get('/products/:id', (req, res) => {
const productId = req.params.id;
res.send(`${productId}번 상품 상세 페이지입니다.`);
});여기서 :id는 "이 자리에 들어오는 값을 id라는 이름으로 기억하겠다"는 뜻입니다. 이런 값을 라우트 파라미터라고 합니다. Express는 주소에서 잡아낸 값을 req.params에 담아 줍니다.1
예를 들어 브라우저에서 다음 주소를 열어 보세요.
http://localhost:3000/products/7그러면 서버는 이렇게 읽습니다.
라우트 모양:
/products/:id실제 요청 주소:
/products/7꺼낸 값:
req.params.id는'7'
브라우저에는 다음과 비슷하게 보입니다.
7번 상품 상세 페이지입니다.이번에는 주소 끝 숫자를 바꿔 보세요.
http://localhost:3000/products/25그러면 응답도 바뀝니다.
25번 상품 상세 페이지입니다.이처럼 라우트 파라미터는 "주소 안에 들어 있는 값"을 꺼낼 때 사용합니다.
검색 조건은 req.query로 읽기
이번에는 검색 주소를 만들어 보겠습니다.
쇼핑몰에서 "coffee"를 검색하고 2페이지를 보고 싶다면 주소가 다음처럼 생길 수 있습니다.
/search?q=coffee&page=2여기서 /search는 주소 경로입니다. 물음표 뒤의 q=coffee&page=2는 쿼리 문자열이라고 부릅니다.
비유하자면 /search는 검색 창구이고, q=coffee&page=2는 창구에 건네는 작은 메모입니다.
q=coffee: 검색어는 coffeepage=2: 페이지는 2
중요한 점이 있습니다. Express에서 라우트 경로는 /search까지만 적습니다. 공식 문서에서도 쿼리 문자열은 라우트 경로의 일부가 아니라고 설명합니다.1
따라서 코드는 이렇게 작성합니다.
app.get('/search', (req, res) => {
const keyword = req.query.q;
const page = req.query.page;
res.send(`검색어는 ${keyword}, 페이지는 ${page}입니다.`);
});req.query는 물음표 뒤에 붙은 값을 담고 있습니다.
이제 브라우저에서 다음 주소를 열어 보세요.
http://localhost:3000/search?q=coffee&page=2성공하면 다음과 비슷한 문장이 보입니다.
검색어는 coffee, 페이지는 2입니다.검색어를 바꾸면 응답도 바뀝니다.
http://localhost:3000/search?q=tea&page=1검색어는 tea, 페이지는 1입니다.만약 q나 page를 빼고 접속하면 해당 값이 undefined로 보일 수 있습니다. 아직 값을 안 보냈기 때문에 생기는 자연스러운 결과입니다. 나중에는 값이 없을 때 기본값을 넣거나 안내 문장을 보여 주도록 다듬을 수 있습니다.
라우트 파라미터와 쿼리 문자열은 처음에 헷갈리기 쉽습니다. 다음처럼 나누어 기억하면 좋습니다.
| 구분 | 예시 | 읽는 곳 | 어울리는 상황 |
|---|---|---|---|
| 라우트 파라미터 | /products/7 | req.params.id | 7번 상품처럼 특정 대상 하나를 가리킬 때 |
| 쿼리 문자열 | /search?q=coffee&page=2 | req.query.q, req.query.page | 검색어, 페이지, 정렬처럼 추가 조건을 줄 때 |
app.js 중간 점검
지금까지 추가한 라우트는 다음과 같은 모양입니다. 기존 파일과 비교하며 빠진 부분이 있는지 확인해 보세요.
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('문의 접수 서버의 첫 화면입니다.');
});
app.get('/about', (req, res) => {
res.send('이 서버는 문의와 상품 정보를 연습하기 위한 서버입니다.');
});
app.get('/products', (req, res) => {
res.send('상품 목록입니다: 커피, 머그컵, 노트');
});
app.get('/products/:id', (req, res) => {
const productId = req.params.id;
res.send(`${productId}번 상품 상세 페이지입니다.`);
});
app.get('/search', (req, res) => {
const keyword = req.query.q;
const page = req.query.page;
res.send(`검색어는 ${keyword}, 페이지는 ${page}입니다.`);
});
app.post('/contact', (req, res) => {
res.send('문의가 제출되었습니다.');
});
app.listen(3000, () => {
console.log('서버가 http://localhost:3000 에서 실행 중입니다.');
});코드를 저장한 뒤 서버를 다시 실행합니다. 이미 서버가 켜져 있다면 터미널에서 Ctrl + C로 끈 다음 다시 실행해 주세요.
node app.js그리고 다음 주소들을 확인합니다.
http://localhost:3000
http://localhost:3000/about
http://localhost:3000/products
http://localhost:3000/products/3
http://localhost:3000/search?q=coffee&page=2각 주소마다 다른 응답이 보이면 라우팅 실습은 성공입니다.
URL 이름을 짓는 작은 규칙
서버 주소는 나중의 나와 다른 사람이 함께 읽는 안내판입니다. 처음부터 완벽할 필요는 없지만, 알아보기 쉬운 이름을 붙이면 프로젝트가 커져도 길을 잃을 가능성이 줄어듭니다.
다음 규칙을 추천합니다.
첫째, 주소는 가능하면 짧고 분명하게 짓습니다.
좋음: /products
아쉬움: /show-all-product-list-page-now둘째, 여러 단어는 소문자와 하이픈으로 연결합니다.
좋음: /contact-form
아쉬움: /ContactForm
아쉬움: /contact form주소에 공백이 들어가면 브라우저가 다른 기호로 바꾸어 처리할 수 있습니다. 초보 단계에서는 공백 없는 주소를 사용하는 편이 안전합니다.
셋째, 비슷한 성격의 주소는 한 가족처럼 묶습니다.
/products
/products/1
/products/2이렇게 만들면 "상품과 관련된 주소는 /products 아래에 있구나"라고 이해하기 쉽습니다.
넷째, 비밀번호나 주민등록번호처럼 민감한 값은 URL에 넣지 않습니다.
피하기: /login?password=1234URL은 브라우저 기록, 서버 기록, 공유된 링크에 남을 수 있습니다. 검색어, 페이지 번호처럼 노출되어도 부담이 작은 값에 쿼리 문자열을 사용하는 편이 좋습니다.
다섯째, 주소에 기능 설명을 너무 많이 넣지 않습니다.
좋음: POST /contact
아쉬움: /send-contact-message-to-adminPOST라는 요청 방식 자체가 "제출한다"는 성격을 담고 있습니다. 그래서 주소에는 주로 대상의 이름을 쓰고, 동작의 성격은 GET, POST 같은 메서드로 나누는 방식이 자주 쓰입니다.
라우팅을 읽는 순서
Express 라우트가 많아지면 코드가 길어집니다. 그럴 때는 다음 순서로 읽어 보세요.
app.get('/products/:id', (req, res) => {
const productId = req.params.id;
res.send(`${productId}번 상품 상세 페이지입니다.`);
});먼저 app.get을 봅니다.
GET 요청이구나. 정보를 조회하는 주소일 가능성이 높다.
다음으로 '/products/:id'를 봅니다.
상품 하나를 가리키는 주소구나.
:id자리는 바뀌는 값이구나.
그다음 (req, res)를 봅니다.
요청 정보를 읽고, 응답을 보내겠구나.
마지막으로 함수 안을 봅니다.
주소에서 id를 꺼내고, 그 id를 이용해 응답 문장을 만드는구나.
코드를 한 번에 모두 이해하려고 하면 부담이 큽니다. "메서드 → 주소 → 요청에서 꺼내는 값 → 응답" 순서로 읽으면 라우팅 코드는 훨씬 차분하게 보입니다.
이번 장에서 기억할 것
라우팅은 서버 안에 여러 접수 창구를 만드는 작업입니다. Express 서버를 만든다는 것은 결국 "어떤 요청이 어느 주소로 들어왔을 때, 어떤 응답을 보낼지"를 차곡차곡 정하는 일입니다.
이번 장에서 배운 핵심은 다섯 가지입니다.
app.get('/'),app.get('/about')처럼 주소별로 다른 응답을 만들 수 있습니다.GET은 주로 조회, POST는 주로 제출에 사용합니다.
/products/:id처럼 주소의 일부를 변수처럼 받을 수 있고, 그 값은req.params에서 꺼냅니다./search?q=coffee&page=2처럼 물음표 뒤에 붙은 조건은req.query에서 읽습니다.URL 이름은 짧고 분명하게, 비슷한 것끼리 묶어서 짓는 것이 좋습니다.
다음 장에서는 요청과 응답 사이에서 일을 도와주는 미들웨어를 배웁니다. 미들웨어는 서버로 들어온 요청이 최종 라우트에 도착하기 전, 중간에서 기록을 남기거나 데이터를 해석하거나 공통 처리를 해 주는 도우미입니다.
참고
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.