[260714 12일차] Node·Express, Groq API

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

# Node·Express & Groq API 학습일지

[백엔드] 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를 호출해 답을 가져오는 미니 챗봇까지 만들었다.

Node.js
런타임require
Express
라우팅app.get/post
CRUD
GETPOSTPUTDELETE
미들웨어
app.usecorsexpress.json
반 배치도 실습
강사 서버 fetch오염 데이터 정정
Groq API
.env 키chat/completions
프록시 챗봇
키는 서버에만fetch 왕복

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)
Node의 http 모듈만으로도 서버를 만들 수 있지만, 주소마다 if/else를 직접 써야 해서 라우트가 늘어날수록 코드가 길어진다. Expressapp.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.jsres.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.bodyundefined
cors()다른 포트(프론트 5173 등)의 요청을 허용
.env + dotenvAPI 키는 서버에만, 화면·깃에는 절대 노출 금지

🔁 회고

잘한 점 — 옆 자리 수강생과 실제 IP로 POST를 주고받는 실습이 가장 기억에 남는다. 그동안 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  |  학습일지