Menu
CoddyTech

검색 페이지

Coddy Node.js 여정의 Express 기초 섹션에 포함된 레슨. 23개 중 23번째.

검색 페이지는 양식이며, 서버는 다른 GET 요청과 마찬가지로 이에 응답합니다.

<form action="/books" method="get">
    <input name="author">
    <button>Find</button>
</form>

이 양식을 제출하면 브라우저는 /books?author= 뒤에 author라는 이름의 입력란에 입력한 내용이 붙은 주소로 이동합니다. 경로는 req.query에서 해당 내용을 읽습니다. 검색 후에도 입력한 단어가 표시되도록 필드의 value에 다시 넣으세요. 방문자가 입력한 내용이므로 이스케이프해야 합니다.

challenge icon

챌린지

쉬움

레시피 보관함에 검색 페이지를 추가합니다. 방문자가 단어를 입력하면 페이지에 제목에 해당 단어가 포함된 레시피가 표시됩니다.

  1. server.js에서 404 처리기 위에 /search용 비동기 경로를 추가하세요. req.query.q에서 단어를 읽고, 값이 없으면 빈 문자열을 사용합니다.
  2. 페이지에는 항상 검색 양식이 있습니다. <form action="/search" method="get">에 q라는 이름의 텍스트 필드와 버튼을 넣으세요. 제출하면 /search?q= 뒤에 단어를 붙인 주소가 열립니다.
  3. 필드에는 검색한 단어가 표시됩니다. 예를 들어 curry를 검색한 후에는 필드에 curry가 표시됩니다. 방문자가 입력한 값이므로 escapeHtml을 거친 단어를 필드의 value 속성에 넣으세요.
  4. 양식 아래에는 loadRecipes()에서 가져온 레시피 중 제목에 해당 단어가 대소문자 구분 없이 포함된 모든 레시피의 링크를 나열하세요. soup와 SOUP 모두 Tomato Soup을 찾습니다. 각 링크는 레시피 페이지로 연결되며, 홈페이지에서처럼 이스케이프된 제목을 표시합니다. 예: <a href="/recipes/1">Tomato Soup</a>.
  5. 단어를 검색했지만 해당 단어가 제목에 포함되지 않으면 목록 대신 No recipes found 텍스트를 표시하세요.

직접 해 보기: Run을 누르고 /search로 요청을 보내세요. 페이지에 빈 양식이 표시됩니다. 필드에 SOUP을 입력하고 버튼을 누르세요. 페이지에 Tomato Soup이 표시되고 필드에도 SOUP이 그대로 표시됩니다. pizza를 검색하세요. No recipes found가 표시됩니다. fish를 검색하세요. Fish & Chips가 올바르게 표시됩니다. 그런 다음 {"title": "Pea Soup", "tags": ["soup"]}을 /api/recipes에 POST하고 soup을 다시 검색하세요. Pea Soup도 목록에 표시됩니다.

직접 해보기

import { escapeHtml } from './html.js';
import { dayOfYear } from './day-of-year.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
import recipesRouter, { loadRecipes } from './routes/recipes.js';
import reviewsRouter from './routes/reviews.js';

const app = express();

app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
});

app.use((req, res, next) => {
    res.set('X-Request-Id', randomUUID());
    next();
});

app.use(express.json());

app.use(express.static('public'));

app.get('/', (req, res) => {
    const items = recipes.map((r) => `<li><a href="/recipes/${r.id}">${escapeHtml(r.title)}</a></li>`);
    res.send(`<link rel="stylesheet" href="/style.css"><h1>Recipe Box</h1><ul>${items.join('')}</ul>`);
});

// type('text')는 일반 텍스트를 전송하므로 각 제목이 별도의 줄에 표시됩니다.
app.get('/recipes', (req, res) => {
    const { tag } = req.query;
    const list = tag ? recipes.filter((recipe) => recipe.tags.includes(tag)) : recipes;
    res.type('text').send(list.map((recipe) => recipe.title).join('\n'));
});

app.get('/recipes/:id', (req, res) => {
    const recipe = recipes.find((r) => r.id === Number(req.params.id));
    if (!recipe) {
        res.status(404).send('No such recipe');
        return;
    }
    res.send(recipe.title);
});

app.use('/api/recipes', recipesRouter);
app.use('/api/recipes/:id/reviews', reviewsRouter);

app.get('/api/today', async (req, res) => {
    const day = dayOfYear(req.query.date);
    if (day === null) {
        res.status(400).json({ error: 'Use a date like 2026-03-01' });
        return;
    }
    const recipes = await loadRecipes();
    res.json(recipes[day % recipes.length]);
});

app.get('/about', (req, res) => {
    res.send('Recipe Box collects family recipes.');
});

app.get('/menu', (req, res) => {
    res.redirect(301, '/recipes');
});

app.get('/menu/:id', (req, res) => {
    res.redirect(301, `/recipes/${req.params.id}`);
});

app.use((req, res) => {
    if (req.path.startsWith('/api/')) {
        res.status(404).json({ error: 'Not found' });
        return;
    }
    res.status(404).send('<h1>Page not found</h1><p><a href="/">Back to Recipe Box</a></p>');
});

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).json({ error: 'Something went wrong' });
});

app.listen(3000, () => {
    console.log('Recipe Box is running on port 3000');
});

Express 기초의 모든 레슨

직접 연습해 보세요: Express 플레이그라운드