NodeJS 웹서버

이승훈·2026년 3월 4일

NodeJS

구조

app
├ /routes
├ /views
├ /files
├ /public
├ /tmps
├ /modules
├ .env
└ index.js

코드

/

.env

SESSION_SECRET=세션키(ex: session-key)
SERVER_PORT=http포트(ex: 3000)
DB_HOST=데이터베이스호스트네임(ex: localhost)
DB_PORT=데이터베이스포트(ex: 3306)
DB_USER=데이터베이스사용자
DB_PASSWORD=데이터베이스비밀번호
DB_DATABASE=데이터베이스이름
HASH_FUNCTION=암호방식(ex: SHA2)
HASH_LENGTH=암호길이(ex: 256)

index.js

import express from 'express';
import ejs from 'ejs';
import dotenv from 'dotenv';

import http from 'http';
import path from 'path';

import middleware from './modules/middleware.js';
import routes from './routes/index.js';

dotenv.config();

const app = express();

app.set('view engine', 'ejs');
app.set('views', `${path.resolve()}/views`);

middleware(app);
app.use(routes);

http.createServer(app).listen(process.env.SERVER_PORT, () => {
  console.log('웹서버가 실행 중입니다.');
});

modules/

middleware.js

import express from 'express';
import session from 'express-session';
import dotenv from 'dotenv';

import path from 'path';

dotenv.config();

const sessionOptions = {
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: false,
    maxAge: 1000 * 60 * 60 * 24
  }
};

async function validateSession(req, res, next) {
  const user = req.session.user;
  const pathName = req.path;
  if (pathName === '/login' || pathName === '/logout' || pathName === '/favicon.ico') {
    return next();
  }
  if (user) {
		//로그인 성공 처리
  } else {
    return res.redirect('/login');
  }
}

function middleware (app) {
  app.use(express.json());
  app.use(express.urlencoded({ extended: true }));
  app.use('/public', express.static(`${path.resolve()}/public`));
  app.use('/tmps', express.static(`${path.resolve()}/tmps`));
  app.use(session(sessionOptions));
  app.use(validateSession);
  app.use('/files', express.static(`${path.resolve()}/files`));
}

export default middleware;

db.js

import knex from 'knex';
import dotenv from 'dotenv';

dotenv.config();

const db = knex({
client: 'mysql2',
connection: {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_DATABASE,
  },
});

export default db;

routes/

index.js

import express from 'express';
const router = express.Router();

import login from './login.js';
router.use('/login', login);

import logout from './logout.js';
router.use('/logout', logout);

import home from './home.js';
router.use('/', home);

export default router;

example.js

import express from 'express';
const router = express.Router();

router.get('/', (req, res) => {
  return res.render('example', { data });
});
router.post('/', (req, res) => {
  return res.status(204).end();
});

export default router;

views/

layout1.ejs

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <link rel="manifest" href="/public/manifest.json">
    <link rel="apple-touch-icon" size="512x512" href="/public/images/ios.png">
    <link rel="stylesheet" href="/public/styles/index.css">
    <% if (typeof styles !== 'undefined') { %>
      <% for (const style of styles) { %>
        <link rel="stylesheet" href="/public/styles/<%= style %>.css">
      <% } %>
    <% } %>
	<title>타이틀</title>
  </head>
  <body>
    <header>
      <svg id="menu_toggle" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M120-240v-80h720v80H120Zm0-200v-80h720v80H120Zm0-200v-80h720v80H120Z"/></svg>
    </header>
    <nav></nav>
    <article>

layout2.ejs

	</article>
    <section><progress id="progress" min="0" max="100"></progress></section>
    <footer></footer>
    <script src="/public/scripts/index.js"></script>
      <% if (typeof scripts !== 'undefined') { %>
        <% for (const script of scripts)  { %>
          <script type="<%= typeof script.type === 'undefined' ? 'text/javascript' : script.type %>" src="/public/scripts/<%= script.src %>.js"></script>
		<% } %>
	  <% } %>
  </body>
</html>

example.ejs

<%- include('layout1', { styles: ['example'] }) -%>
<%- include('layout2', { scripts: [{ src: 'example', type: 'module' }] }) -%>

public/

manifest.json

{
  "name": "앱이름",
  "start_url": "/",
  "display": "fullscreen",
  "icons": [
    {
      "src": "images/android.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

styles/

index.css

:root {
  --color-primary: #7bbfa7;
  --color-primary-dark: #2f5d4e;
  --color-primary-light: #d0f0e3;

  --color-gray-900: #111827;
  --color-gray-700: #374151;
  --color-gray-500: #6b7280;
  --color-gray-300: #d1d5db;
  --color-gray-100: #f3f4f6;

  --color-text: #111827;
  --color-text-muted: #6b7280;

  --header-height: 50px;
  --footer-height: 30px;

  --padding: 0.5em;
}

* {
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

header {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: var(--header-height);
}

nav {
  position: fixed;
  left: 0;
  top: var(--header-height);
  height: calc(100% - var(--header-height) - var(--footer-height));
  display: flex;
  flex-direction: column;
}
nav a {
  padding: var(--padding);
  color: var(--color-text-muted);
  text-align: center;
}
nav a:hover {
  background: var(--color-gray-100);
  color: var(--color-text);
}

section,
article {
  position: fixed;
  top: var(--header-height);
  left: 0;
  width: 100%;
  height: calc(100% - var(--header-height) - var(--footer-height));
}

article {
  z-index: 2;
  overflow: auto;
}

section {
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1;
}

footer {
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  height: var(--footer-height);
}

a {
  text-decoration: none;
}

.sticky {
  position: sticky;
}

@media only screen and (orientation: landscape) {
}

@media only screen and (orientation: portrait) {
}

미들웨어에 함수 정의

app.locals.todayOrTomorrow = (inputDateStr) => {
  const seoul = DateTime.now().setZone('Asia/Seoul');
  const today = seoul.toISODate();
  const tomorrow = seoul.plus({ days: 1 }).toISODate();
  if (inputDateStr === today) return '오늘';
  if (inputDateStr === tomorrow) return '내일';
  return '해당 없음';
};
profile
안녕하세요!

0개의 댓글