입력한 내용을 서버가 받는다는 뜻
지금까지는 주로 서버가 브라우저에게 무언가를 보내는 흐름을 보았습니다. 이제 방향을 한 번 바꿔 보겠습니다. 이번에는 브라우저가 서버에게 내용을 보냅니다.
문의하기, 예약하기, 신청하기 화면을 떠올려 보세요. 사용자는 이름, 이메일, 메시지를 입력하고 버튼을 누릅니다. 그러면 브라우저는 그 입력값을 작은 봉투에 담아 서버의 특정 주소로 보냅니다. 서버는 그 봉투를 열어 내용을 읽고, 필요한 처리를 한 뒤 응답을 돌려줍니다.
HTML에서는 이 일을 form 태그가 맡습니다. Express 서버에서는 보통 app.post() 라우트가 이 제출을 받습니다.
이번 장에서는 작은 문의 폼을 만듭니다.
사용자가 이름, 이메일, 메시지를 입력합니다.
브라우저가
/contact주소로 폼을 제출합니다.Express 서버가 입력값을
req.body에서 읽습니다.빈 값이 있으면 다시 안내합니다.
정상 제출이면 확인 메시지를 보여 줍니다.
AI 생성 이미지
form의 action과 method 읽기
폼은 서버로 보내는 종이 신청서와 비슷합니다. 신청서에는 "어디로 보낼지"와 "어떤 방식으로 보낼지"가 적혀 있어야 합니다.
HTML에서는 이렇게 씁니다.
<form action='/contact' method='POST'>
...
</form>여기서 action은 제출할 서버 주소입니다. 위 코드는 폼 내용을 /contact 주소로 보내겠다는 뜻입니다.
method는 요청 방식입니다. 사용자가 내용을 제출하는 상황이므로 POST를 사용합니다. 앞에서 GET은 주로 조회, POST는 주로 제출에 어울린다고 했습니다.
이 HTML 폼과 연결되는 Express 코드는 다음 모양이 됩니다.
app.post('/contact', (req, res) => {
res.send('문의가 도착했습니다.');
});둘을 나란히 보면 연결이 보입니다.
<form action='/contact' method='POST'>app.post('/contact', ...)action='/contact'와 app.post('/contact')가 같은 주소를 바라봅니다. method='POST'와 app.post()도 같은 방식입니다. 폼 제출이 길을 잃지 않으려면 이 둘이 서로 맞아야 합니다.
name 속성은 서버에서 읽을 이름표다
폼 안의 입력칸에는 name 속성이 필요합니다. name은 서버가 입력값을 꺼낼 때 사용하는 이름표입니다.
<input name='email'>사용자가 이 칸에 hello@example.com을 입력했다면, 서버에서는 다음처럼 읽을 수 있습니다.
req.body.email즉, name='email'의 email이 req.body.email의 email이 됩니다.
이 관계가 중요합니다.
<input name='username'>위처럼 쓰면 서버에서는 req.body.username으로 읽어야 합니다.
<input name='userEmail'>위처럼 쓰면 서버에서는 req.body.userEmail로 읽어야 합니다.
입력칸 옆에 적는 화면 표시 문구는 사용자가 보는 안내입니다. 반면 name은 서버가 읽는 이름표입니다. 폼이 서버에 잘 도착했는데 값이 undefined로 보인다면, 먼저 HTML의 name과 서버의 req.body.이름이 같은지 확인해 보세요.
문의 폼 HTML 만들기
이제 직접 만들어 보겠습니다.
우리는 이름, 이메일, 문의 내용을 입력하는 contact.html 파일을 만들고, 이 폼을 Express 서버의 /contact 라우트로 보낼 것입니다.
먼저 VS Code에서 프로젝트 폴더 express-practice를 엽니다. 이미 사용 중인 프로젝트 폴더가 있다면 그대로 이어서 사용하면 됩니다. 처음 만드는 중이라면 아래처럼 views 폴더를 만들고 그 안에 contact.html 파일을 새로 만드세요.
express-practice
├─ app.js
├─ views
│ └─ contact.html
└─ publicpublic 폴더는 지금 당장 사용하지 않아도 괜찮습니다. 이미 있다면 그대로 두고, 없다면 이번 장에서는 만들지 않아도 됩니다. 중요한 것은 app.js와 views/contact.html의 위치입니다.
views/contact.html 파일에 기본 HTML을 작성합니다.
<!DOCTYPE html>
<html lang='ko'>
<head>
<meta charset='UTF-8'>
<title>문의하기</title>
</head>
<body>
<h1>문의하기</h1>
</body>
</html>아직 폼은 없습니다. 먼저 화면의 기본 틀만 만든 상태입니다.
이제 body 안에 폼을 추가합니다.
<form action='/contact' method='POST'>
</form>이 줄은 "이 폼을 제출하면 /contact 주소로 POST 요청을 보내겠다"는 뜻입니다.
그 안에 이름 입력칸을 넣습니다.
<label>
이름
<input type='text' name='name'>
</label>label은 입력칸의 설명입니다. 사용자가 무엇을 입력해야 하는지 알려 줍니다. 여기서 서버가 읽을 이름표는 name='name'입니다.
이메일 입력칸도 추가합니다.
<label>
이메일
<input type='email' name='email'>
</label>type='email'은 브라우저에게 "이 칸은 이메일 형식이 어울린다"고 알려 줍니다. 다만 이것만 믿으면 안 됩니다. 브라우저 검사는 사용자가 우회할 수 있으므로 서버에서도 다시 확인해야 합니다.
문의 내용을 길게 입력할 칸은 textarea를 사용합니다.
<label>
문의 내용
<textarea name='message'></textarea>
</label>마지막으로 제출 버튼을 넣습니다.
<button type='submit'>문의 보내기</button>views/contact.html의 전체 모양은 다음과 비슷하면 됩니다.
<!DOCTYPE html>
<html lang='ko'>
<head>
<meta charset='UTF-8'>
<title>문의하기</title>
</head>
<body>
<h1>문의하기</h1>
<form action='/contact' method='POST'>
<p>
<label>
이름<br>
<input type='text' name='name'>
</label>
</p>
<p>
<label>
이메일<br>
<input type='email' name='email'>
</label>
</p>
<p>
<label>
문의 내용<br>
<textarea name='message'></textarea>
</label>
</p>
<button type='submit'>문의 보내기</button>
</form>
</body>
</html>서버에서 문의 페이지 보여 주기
이제 app.js를 엽니다. 이번에는 /contact 주소로 접속했을 때 방금 만든 HTML 파일을 보여 주겠습니다.
먼저 위쪽에 path를 불러오는 코드가 있는지 확인합니다.
const path = require('path');이미 추가되어 있다면 다시 쓰지 않아도 됩니다.
그리고 문의 페이지를 보여 주는 GET 라우트를 추가합니다.
app.get('/contact', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'contact.html'));
});이 코드는 "브라우저가 /contact 주소를 조회하면 views/contact.html 파일을 보내라"는 뜻입니다.
터미널에서 서버를 실행합니다.
node app.js브라우저에서 다음 주소로 접속합니다.
http://localhost:3000/contact성공하면 문의하기 제목과 이름, 이메일, 문의 내용 입력칸이 보입니다. 아직 제출 처리는 만들지 않았기 때문에 버튼을 누르면 Cannot POST /contact 같은 문구가 보일 수 있습니다. 이것은 자연스러운 중간 단계입니다. 지금은 화면을 준비한 것입니다.
폼 내용을 읽기 위한 미들웨어 추가하기
브라우저가 폼을 제출하면 입력값은 요청 본문에 담겨 옵니다. 요청 본문은 택배 상자 안쪽에 들어 있는 실제 내용물과 비슷합니다.
Express가 이 내용물을 항상 바로 읽을 수 있는 것은 아닙니다. 상자를 열어 정리해 주는 도우미가 필요합니다. 이 역할을 하는 것이 express.urlencoded() 미들웨어입니다.
app.js에서 const app = express(); 아래, 그리고 폼 제출을 받는 라우트보다 위쪽에 다음 코드를 추가합니다.
app.use(express.urlencoded({ extended: true }));위치는 중요합니다. app.post('/contact', ...)보다 먼저 있어야 합니다. 그래야 Express가 요청 본문을 해석한 뒤 req.body에 담아 줍니다.
흐름은 이렇습니다.
브라우저가 폼을 제출합니다.
express.urlencoded()가 요청 본문을 해석합니다.해석된 값이
req.body에 들어갑니다./contactPOST 라우트에서req.body.name,req.body.email,req.body.message를 읽습니다.
만약 나중에 req.body가 비어 있거나 undefined처럼 보인다면, 먼저 이 미들웨어가 있는지, 그리고 POST 라우트보다 위에 있는지 확인해 보세요.
POST 라우트에서 입력값 받기
이제 폼 제출을 받을 라우트를 만듭니다. app.js에 다음 코드를 추가합니다.
app.post('/contact', (req, res) => {
const name = req.body.name;
const email = req.body.email;
const message = req.body.message;
res.send('문의가 접수되었습니다.');
});한 줄씩 보겠습니다.
app.post('/contact', ...)POST 방식으로 /contact에 들어온 요청을 처리합니다. HTML 폼의 action과 method가 이 라우트로 연결됩니다.
const name = req.body.name;폼에서 name='name'인 입력값을 꺼냅니다.
const email = req.body.email;폼에서 name='email'인 입력값을 꺼냅니다.
const message = req.body.message;폼에서 name='message'인 입력값을 꺼냅니다.
지금은 값을 꺼내기만 하고 저장하지는 않습니다. 먼저 서버가 폼 데이터를 받을 수 있는지 확인하는 단계입니다.
서버를 이미 실행 중이라면 터미널에서 Ctrl + C로 끈 뒤 다시 실행합니다.
node app.js브라우저에서 http://localhost:3000/contact로 접속한 뒤, 값을 입력하고 버튼을 눌러 보세요.
성공하면 브라우저에 다음 문장이 보입니다.
문의가 접수되었습니다.이 화면이 보이면 폼 제출이 서버까지 도착한 것입니다.
입력값을 확인 응답에 넣기
사용자에게 조금 더 친절한 확인 화면을 보여 주겠습니다. 받은 이름과 메시지를 이용해 응답을 만들어 봅니다.
app.post('/contact', ...) 안의 res.send() 부분을 다음처럼 바꿉니다.
res.send(`
<h1>문의가 접수되었습니다</h1>
<p>${name}님, 문의해 주셔서 감사합니다.</p>
<p>입력하신 이메일: ${email}</p>
<p>문의 내용:</p>
<p>${message}</p>
<p><a href='/contact'>다시 작성하기</a></p>
`);이 코드는 서버가 HTML 조각을 직접 만들어 응답하는 방식입니다. 지금은 학습을 위해 간단히 사용합니다.
다시 서버를 실행하고 폼을 제출해 보세요. 성공하면 사용자가 입력한 이름, 이메일, 문의 내용이 확인 화면에 보입니다.
다만 실제 서비스에서는 사용자가 입력한 값을 그대로 HTML에 넣는 일을 조심해야 합니다. 악의적인 사용자가 HTML이나 JavaScript 코드를 입력할 수도 있기 때문입니다. 이 문제는 보안과 관련이 있으므로 뒤의 보안 장에서 다시 다룹니다. 지금은 "사용자 입력값은 항상 조심해서 다룬다"는 감각만 가져도 충분합니다.
조금 더 안전하게 연습하려면, 화면에 다시 보여 줄 값은 HTML에서 특별한 의미가 있는 문자들을 평범한 글자로 바꿔 준 뒤 넣는 것이 좋습니다. 아래쪽의 최종 코드에서는 escapeHtml()이라는 작은 함수를 사용해 이 처리를 넣어 보겠습니다. 함수 내용이 아직 낯설다면, "사용자가 쓴 < 같은 문자를 HTML 태그로 해석하지 않게 바꾸는 장치"라고 이해하면 됩니다.
빈 값 제출 막기
브라우저에서 입력칸을 비워 두고 제출해 보세요. 지금 서버는 빈 값도 그대로 접수합니다. 실제 문의 접수에서는 이름, 이메일, 메시지가 모두 필요합니다.
HTML 입력칸에 required를 붙이면 브라우저가 빈 값 제출을 한 번 막아 줍니다.
<input type='text' name='name' required>
<input type='email' name='email' required>
<textarea name='message' required></textarea>contact.html의 입력칸에도 이 속성을 붙여 두면 좋습니다. 하지만 브라우저의 검사만 믿으면 안 됩니다. 사용자가 브라우저 검사를 끌 수도 있고, 다른 도구로 직접 요청을 보낼 수도 있습니다. 서버는 마지막 접수 담당자라고 생각하면 됩니다. 접수 담당자는 신청서가 제대로 채워졌는지 직접 확인해야 합니다.
app.post('/contact', ...) 안에서 값을 꺼낸 다음, 응답을 보내기 전에 검사를 추가합니다.
if (!name || !email || !message) {
return res.status(400).send(`
<h1>입력값을 확인해 주세요</h1>
<p>이름, 이메일, 문의 내용을 모두 입력해야 합니다.</p>
<p><a href='/contact'>문의 폼으로 돌아가기</a></p>
`);
}!name은 name 값이 비어 있는지 확인합니다. email과 message도 같은 방식입니다.
여기서 return을 붙인 이유도 중요합니다. 오류 응답을 보낸 뒤 함수 실행을 멈추기 위해서입니다. 한 요청에는 응답을 한 번만 보내야 합니다. return이 없으면 아래쪽의 성공 응답까지 이어서 보내려다 오류가 날 수 있습니다.
조금 더 안전하게 하려면 앞뒤 공백만 입력한 경우도 빈 값으로 보아야 합니다. 사용자가 스페이스바만 여러 번 누른 경우입니다.
값을 꺼내는 부분을 다음처럼 바꿀 수 있습니다.
const name = req.body.name.trim();
const email = req.body.email.trim();
const message = req.body.message.trim();trim()은 문자열 앞뒤의 빈칸을 제거합니다. 예를 들어 ' 민지 '는 '민지'가 됩니다.
다만 req.body.name 자체가 없을 가능성까지 생각하면 더 안전하게 이렇게 쓸 수도 있습니다.
const name = (req.body.name || '').trim();
const email = (req.body.email || '').trim();
const message = (req.body.message || '').trim();처음에는 길어 보일 수 있습니다. 뜻은 단순합니다. 값이 있으면 그 값을 쓰고, 값이 없으면 빈 문자열로 바꾼 뒤 공백을 정리합니다.
app.js의 핵심 코드 정리
이번 장에서 중요한 app.js 부분만 모아 보면 다음과 같습니다. 기존에 다른 설정이나 다른 라우트가 있다면 그대로 두고, 이 흐름이 들어가 있으면 됩니다.
const express = require('express');
const path = require('path');
const app = express();
const PORT = 3000;
app.use(express.urlencoded({ extended: true }));
function escapeHtml(text) {
return text
.replaceAll('&', '&')
.replaceAll('<', '<')
.replaceAll('>', '>')
.replaceAll('"', '"')
.replaceAll("'", ''');
}
app.get('/contact', (req, res) => {
res.sendFile(path.join(__dirname, 'views', 'contact.html'));
});
app.post('/contact', (req, res) => {
const name = (req.body.name || '').trim();
const email = (req.body.email || '').trim();
const message = (req.body.message || '').trim();
if (!name || !email || !message) {
return res.status(400).send(`
<h1>입력값을 확인해 주세요</h1>
<p>이름, 이메일, 문의 내용을 모두 입력해야 합니다.</p>
<p><a href='/contact'>문의 폼으로 돌아가기</a></p>
`);
}
res.send(`
<h1>문의가 접수되었습니다</h1>
<p>${escapeHtml(name)}님, 문의해 주셔서 감사합니다.</p>
<p>입력하신 이메일: ${escapeHtml(email)}</p>
<p>문의 내용:</p>
<p>${escapeHtml(message)}</p>
<p><a href='/contact'>다시 작성하기</a></p>
`);
});
app.listen(PORT, () => {
console.log(`서버 실행 중: http://localhost:${PORT}`);
});여기서 escapeHtml()은 사용자가 입력한 글자를 화면에 다시 보여 줄 때를 대비한 최소한의 안전장치입니다. 지금 단계에서 함수 내부를 완벽히 외울 필요는 없습니다. 중요한 것은 사용자가 입력한 값을 HTML 안에 그대로 넣는 습관이 위험할 수 있다는 점입니다.
서버를 다시 실행합니다.
node app.js브라우저에서 확인합니다.
http://localhost:3000/contact성공하면 다음 흐름을 확인할 수 있습니다.
문의 폼이 보입니다.
값을 입력하고 제출합니다.
서버가 값을 받아 확인 화면을 보여 줍니다.
빈 값이 있으면 안내 메시지를 보여 줍니다.
contact.html도 마지막으로 정리하기
HTML 파일도 빈 값 제출을 한 번 막아 주도록 required를 붙여 정리해 두겠습니다.
<!DOCTYPE html>
<html lang='ko'>
<head>
<meta charset='UTF-8'>
<title>문의하기</title>
</head>
<body>
<h1>문의하기</h1>
<form action='/contact' method='POST'>
<p>
<label>
이름<br>
<input type='text' name='name' required>
</label>
</p>
<p>
<label>
이메일<br>
<input type='email' name='email' required>
</label>
</p>
<p>
<label>
문의 내용<br>
<textarea name='message' required></textarea>
</label>
</p>
<button type='submit'>문의 보내기</button>
</form>
</body>
</html>다시 말하지만 required는 브라우저 쪽에서 먼저 도와주는 장치입니다. 서버 쪽의 if (!name || !email || !message) 검사는 여전히 필요합니다. 같은 검사를 두 번 하는 것처럼 보여도 역할이 다릅니다. 브라우저 검사는 사용자를 편하게 해 주고, 서버 검사는 서버를 안전하게 지켜 줍니다.
개인정보를 콘솔에 함부로 찍지 않기
학습 중에는 console.log(req.body)를 찍어 보고 싶을 수 있습니다. 실제로 값이 들어오는지 확인하는 데 도움이 됩니다.
하지만 문의 폼에는 이름, 이메일, 전화번호, 문의 내용처럼 개인정보가 들어올 수 있습니다. 콘솔에 찍힌 내용은 터미널 기록, 서버 로그, 배포 환경의 로그 수집 도구에 남을 수 있습니다. 내 컴퓨터에서 혼자 실습할 때는 작아 보이는 습관이 실제 서비스에서는 큰 문제가 될 수 있습니다.
확인이 꼭 필요하다면 전체 내용을 찍기보다 형태만 확인하세요.
console.log('문의 폼 제출 도착');또는 민감하지 않은 정보만 제한적으로 확인합니다.
console.log('입력된 항목:', Object.keys(req.body));이렇게 하면 실제 이름이나 이메일을 노출하지 않고도 어떤 필드가 들어왔는지 볼 수 있습니다.
서버는 사용자의 정보를 맡는 자리입니다. 저장하지 않는 실습 코드에서도 이 감각을 일찍 익혀 두면 좋습니다.
이번 장에서 익힌 것
이번 장에서는 브라우저에서 입력한 값을 Express 서버가 받는 흐름을 만들었습니다.
핵심은 세 가지입니다.
첫째, HTML 폼의 action과 method는 서버 라우트와 맞아야 합니다. action='/contact' method='POST'는 Express의 app.post('/contact')와 연결됩니다.
둘째, 입력칸의 name 속성은 서버에서 req.body의 키 이름이 됩니다. name='email'이면 서버에서는 req.body.email로 읽습니다.
셋째, 서버는 입력값을 믿기 전에 확인해야 합니다. 빈 값 검사와 공백 정리, 그리고 개인정보를 로그에 함부로 남기지 않는 습관은 가장 기본적인 서버 측 검증과 안전 수칙입니다.
이제 서버는 단순히 페이지를 보여 주는 역할을 넘어, 사용자가 보낸 내용을 받을 수 있게 되었습니다. 다음 장에서는 화면이 아니라 데이터 자체를 주고받는 방식, 즉 JSON API를 만들어 보겠습니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.