2026. 7. 14. 16:44ㆍAI 기반 프론트엔드 웹개발자 양성과정

[백엔드] Node.js·Express로 서버 만들기
+ Groq API로 내 서버에 AI 챗봇 붙이기
2026.07.14(화) · 07_node_express (CRUD 실습) · 08_groq (AI 연동)
📋 오늘 다룬 것들
그동안은 브라우저 안에서만 움직이는 화면(HTML·CSS·JS·React)을 만들었다면, 오늘은 처음으로 서버를 직접 켜봤다. Node.js로 JS를 브라우저 밖에서 돌리는 법, Express로 라우팅·CRUD API를 만드는 법, 그리고 강사 서버에서 우리 반 20명 데이터를 받아와 오염된 걸 고치는 실습까지 진행했다. 오후에는 Groq API 키를 발급받아 내 서버에 AI를 연결하고, 화면에서 물어보면 서버가 대신 Groq를 호출해 답을 가져오는 미니 챗봇까지 만들었다.
1. Node.js & Express — 서버의 첫걸음
Node.js는 브라우저 밖에서 JS를 실행하는 런타임이다. 브라우저 JS는 화면 꾸미기만 할 수 있고 파일·DB·API 키에는 접근할 수 없는데(보안), Node 환경에서는 그게 다 가능하다 — 그래서 서버·백엔드 일은 Node로 한다는 걸 먼저 확인했다.
// 07_node_express/01/index.js console.log("Hello") // 터미널 창 열기 : Ctrl + ~ const os = require("os") // Node 내장 모듈, 설치 불필요 console.log("CPU 개수 : ", os.cpus().length)
http 모듈만으로도 서버를 만들 수 있지만, 주소마다 if/else를 직접 써야 해서 라우트가 늘어날수록 코드가 길어진다. Express는 app.get('/주소', 함수) 한 줄로 그걸 대신해주는 프레임워크 — 라우트 100개도 나란히 한 줄씩 깔끔하게 늘어난다.| 용어 | 정체 |
|---|---|
| 런타임 | 코드 실행 환경 → Node.js |
| 프레임워크 | 정해진 틀에 코드를 끼움 → Express |
| 프로토콜 | 통신 규칙(약속) → HTTP |
2. CRUD API 만들기
① GET — 고정 주소 / 변하는 주소(:id)
주소가 항상 같으면 전체 목록, 주소 끝에 :id처럼 변하는 값을 넣으면 그 항목 하나만 조회한다. req.params로 꺼낸 값은 문자열이라 비교 전에 Number()로 바꿔야 한다는 걸 여기서 다시 확인했다.
// index_04.js — 고정 주소: 전체 목록 app.get('/api/users', (req, res) => { res.json([ { id: 1, name: "KIM" }, { id: 2, name: "LEE" } ]) }) // index_05.js — 변하는 주소: 특정 1명 조회 app.get('/api/users/:id', (req, res) => { const user = users.find(u => u.id === Number(req.params.id)) if(!user) return res.status(404).json({ error: '없는 유저' }) res.json(user) })
② POST — 옆 자리 수강생과 진짜로 주고받기
가장 재미있었던 실습. POST는 body에 내용을 실어 보내는 방식이라 express.json()이 꼭 있어야 req.body가 풀린다. 짝을 정해서 한 명은 서버(A), 한 명은 요청 보내는 쪽(B)이 되어 같은 LAN(교실 와이파이) 안에서 진짜 IP로 메시지를 주고받는 실습을 했다.
// index_06_server.js — 내가 켠 서버 (받는 쪽) const express = require('express') const app = express() app.use(express.json()) // 必 — 없으면 req.body가 undefined app.post('/api/chat', (req, res) => { const { message } = req.body console.log('받은 메시지:', message) res.json({ ok: true, 받은문장: message }) }) app.listen(3000, () => console.log('http://192.168.10.22:3000'))
// index_06_front.js — 옆 자리(짝)의 서버로 메시지 보내기 const readline = require('readline') const rl = readline.createInterface({ input: process.stdin, output: process.stdout }) rl.question('메시지: ', (message) => { fetch('http://192.168.20.27:3000/api/chat', { // ← 짝꿍 IP method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message }) }) .then(r => r.json()) .then(console.log) .catch(() => console.log('❌ 서버 먼저 켜기')) .finally(() => rl.close()) })
192.168.10.22가 찍히는데, 실제로 메시지를 보낼 땐 192.168.20.27(짝의 IP)로 보냈다 — 처음엔 "내 IP로 보내면 되는 거 아닌가?" 헷갈렸는데, 내가 요청을 받는 사람이면 내 IP를, 남에게 요청을 보내는 사람이면 상대 IP를 써야 한다는 걸 직접 헤매면서 이해했다. localhost는 항상 "내 컴퓨터 자신"만 가리킨다는 것도 이번에 확실해졌다.③ PUT — 수정 / ④ DELETE — 삭제
PUT은 :id로 항목을 찾아 값을 바꾸고, DELETE는 filter로 그 id만 뺀 새 배열을 만든다. DELETE는 보낼 내용(body)이 없어서 express.json()이 굳이 필요 없다는 것도 오늘 알았다.
// index_07.js — PUT (수정) app.put('/api/users/:id', (req, res) => { const u = users.find(u => u.id === Number(req.params.id)) if (!u) return res.status(404),json({error: "없는 유저입니다."}) // ← 오타! .json이어야 하는데 콤마(,)를 찍음 u.name = req.body.name res.json(u) })
// index_08.js — DELETE (삭제, body 없음) app.delete('/api/users/:id', (req, res) => { users = users.filter(u => u.id !== Number(req.params.id)) res.json({ok: true, 남은: users}) })
index_07.js에 res.status(404),json(...)처럼 점(.) 대신 콤마(,)를 찍은 오타가 그대로 남아 있다. 이 상태로는 존재하지 않는 유저 분기를 타는 순간 SyntaxError가 난다 — 평소엔 잘 되다가 "없는 id"를 테스트할 때만 터지는 케이스라 자칫 놓치기 쉬운 실수였다. .(메서드 체이닝)과 ,(쉼표 나열)를 눈으로 훑을 땐 정말 비슷하게 생겨서, 에러 메시지 줄 번호부터 먼저 확인하는 습관이 왜 중요한지 체감했다.⑤ 미들웨어 · CORS
미들웨어는 요청이 라우트에 도착하기 전에 거쳐가는 공통 처리 칸이다. cors()는 다른 포트에서 오는 요청을 허용하는 스위치, 직접 만든 로그 미들웨어는 모든 요청의 메서드·주소를 콘솔에 남긴다.
// index_09.js const cors = require("cors") app.use(cors()) // 다른 포트 허용 app.use(express.json()) app.use((req, res, next) => { console.log(req.method, req.url) // 모든 요청 로그 next() // 다음으로 넘기기 (안 부르면 여기서 멈춤) })
next()를 안 부르면 그 자리에서 멈춰버린다. cors()·express.json()·로그, 검색대 3개를 위→아래 순서로 통과해야 app.get/post에 도착한다.3. 반 배치도 만들기 — 강사 서버로 진짜 CRUD 실습
오늘의 하이라이트. 강사 서버(http://192.168.10.28:5000/hancom/한윤지/users)에 우리 반 20명 데이터가 들어있는데, 일부 이름이 오염(오타·중복·누락)돼 있었다. 이걸 fetch로 받아와 정상 명단과 비교하고, PUT·DELETE·POST로 직접 고쳐서 화면에 6열 그리드 배치도로 그렸다.
// index_10.js const BASE = 'http://192.168.10.28:5000/hancom/한윤지/users' const HEADERS = { 'Authorization': 'HANCOM', 'Content-Type': 'application/json' } async function loadStudents() { const res = await fetch(BASE, { headers: HEADERS }) const students = await res.json() render(students) } function render(students) { const grid = document.getElementById('grid') grid.innerHTML = '' students.forEach(s => { const div = document.createElement('div') div.textContent = s.name // 이름 클릭 → 새 이름 입력받아 PUT으로 수정 → 다시 불러오기 div.onclick = async () => { const newName = prompt('새 이름을 입력하세요', s.name) if(!newName) return await fetch(`${BASE}/${s.id}`, { method: 'PUT', headers: HEADERS, body: JSON.stringify({ name:newName }) }) loadStudents() // 수정 후 목록 다시 그리기 } grid.appendChild(div) }) } loadStudents()
Authorization: HANCOM 헤더를 빠뜨리면 조회 자체가 거부된다는 걸 처음 겪었다 — 지금까지 만든 CRUD는 인증이 없는 연습용이었는데, 오늘은 처음으로 "키가 있어야 들어갈 수 있는" 진짜 API를 다뤄봤다. 클릭 한 번 → prompt → PUT → 다시 loadStudents()로 새로고침하는 흐름이, 결국 미니 프로젝트(트래블틱·Culture Log)에서 계속 써온 "데이터 바뀌면 다시 그리기" 패턴과 똑같다는 걸 새삼 느꼈다.4. Groq API — 내 서버에 AI 붙이기
오후엔 Groq API 키를 발급받아 내 서버가 대신 AI를 호출하고, 화면은 그 서버에만 물어보는 구조를 만들었다. 키를 화면(브라우저) 코드에 넣으면 F12로 누구나 볼 수 있어서, 반드시 서버(.env)에만 둬야 한다는 게 핵심이었다.
// 08_groq/.env (절대 깃에 올리지 않음)
GROQ_API_KEY=gsk_••••••••••••••••••••
// 08_groq/index.js — 서버가 키를 들고 Groq를 대신 호출 require('dotenv').config() const express = require('express') const app = express() const key = process.env.GROQ_API_KEY app.use(express.json()) app.use(express.static('public')) app.post('/chat', async (req, res) => { const { message } = req.body const groqRes = await fetch("https://api.groq.com/openai/v1/chat/completions", { method: "POST", headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${key}` }, body: JSON.stringify({ model: 'llama-3.1-8b-instant', messages: [{ role: "user", content: message }] }) }) const data = await groqRes.json() const answer = data.choices?.[0]?.message?.content || "응답 없음" res.json({ answer }) }) app.listen(3000, () => console.log('http://localhost:3000'))
data.choices?.[0]?.message?.content || "응답 없음" — 옵셔널 체이닝(?.)으로 choices부터 content까지 한 단계씩 안전하게 파고들고, 혹시 구조가 이상해 undefined가 나오면 ||로 "응답 없음"이라는 기본값을 대신 보여준다. AI 응답처럼 구조가 복잡한 데이터를 다룰 때 왜 이 조합을 쓰는지 실감났다.// 08_groq/app.js — 화면(client) 쪽 동작 send.addEventListener('click', () => { answer.textContent = '생각 중...' const prompt = q.value fetch('http://localhost:3000/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json'}, body: JSON.stringify({ prompt }) }) .then(res => res.json()) .then(data => {answer.textContent = data.reply || data.error }) .catch(() => {answer.textContent = 'X 서버 안 켜짐? (server서 node index.js 먼저)' }) })
app.js는 /api/chat으로, 그리고 { prompt }라는 키로 요청을 보내는데, 정작 index.js 서버는 /chat 주소에서 { message }라는 키를 꺼내고 응답도 { answer }로 돌려준다. 프론트와 서버가 주소·필드 이름이 서로 안 맞아 지금 이대로는 챗봇이 응답을 못 받는다. F12 콘솔에서 404나 undefined가 뜨면 십중팔구 이렇게 프론트와 서버가 "같은 이름으로 부르기로" 약속을 안 지킨 경우라는 걸 직접 겪으며 배웠다. 다음엔 주소·요청 필드·응답 필드 세 가지를 표로 적어두고 맞춰나가야겠다.
5. 배운 것 정리
| 배운 것 | 핵심 |
|---|---|
| Node.js | 브라우저 밖에서 JS 실행, 파일·DB·API 키 접근 가능 |
| Express 라우팅 | app.get/post/put/delete('주소', 콜백) |
| :id 파라미터 | req.params.id는 문자열 → Number() 변환 필수 |
| express.json() | 없으면 POST/PUT의 req.body가 undefined |
| cors() | 다른 포트(프론트 5173 등)의 요청을 허용 |
| .env + dotenv | API 키는 서버에만, 화면·깃에는 절대 노출 금지 |
🔁 회고
localhost로만 테스트하다 보니 "서버-클라이언트가 다른 컴퓨터에 있다"는 감각이 없었는데, 오늘 처음으로 그걸 몸으로 확인했다. 강사 서버의 오염된 반 데이터를 fetch + CRUD로 직접 고치는 실습도, 지금까지 배운 GET·PUT·DELETE를 한 번에 다 써보는 좋은 마무리였다.
index_07.js의 ,json 오타, 08_groq의 /api/chat ↔ /chat 주소 불일치)을 아직 고치지 못한 채로 하루를 마무리했다. 둘 다 "에러가 안 나다가 특정 상황에서만 터지는" 유형이라 테스트를 꼼꼼히 안 하면 그냥 넘어가기 쉽다는 걸 배웠다. 내일은 이 두 버그부터 고치고 시작해야겠다.
✍️ 마치며
지금까지는 "화면에서 어떻게 보이나"만 다뤘는데, 오늘 처음으로 "요청이 어디서 와서 어디로 가는지"를 눈으로 쫓아가 봤다. 특히 미들웨어를 검색대에 비유한 설명이 계속 머리에 남는다 — cors()·express.json()·로그, 이 세 개를 순서대로 통과해야 라우트에 도착한다는 그림이 그려지고 나니 서버 코드가 훨씬 덜 낯설어졌다. 다음엔 오늘 만난 두 버그를 고치고, Groq 챗봇을 실제로 끝까지 동작시키는 것부터 시작하려 한다.
🏷️ Node.js | Express | CRUD | 미들웨어 | Groq API | 학습일지
'AI 기반 프론트엔드 웹개발자 양성과정' 카테고리의 다른 글
| [260716 14일차] Anaconda, Python (0) | 2026.07.16 |
|---|---|
| [260715 13일차] Vercel, Claude Web (0) | 2026.07.15 |
| [260713 11일차] Mini Project #3 (1) | 2026.07.13 |
| [260710 10일차] React (0) | 2026.07.10 |
| [260709 09일차] React (0) | 2026.07.09 |