Node.js 교과서 : 6. 익스프레스 웹 서버 만들기

전지연·2025년 10월 31일

GDGoC_2025

목록 보기
7/16
post-thumbnail

6-1. express 프로젝트 시작

http는 웹 서버를 만들 때 코드가 보기 좋지 않고, 확장성도 떨어짐
-> 프레임워크로 해결
ex) Express, Koa(코아), Hapi(하피)

package.json 만들기

직접 생성 or npm init 명령어 사용

app.js 작성하기

app.js : 서버 구동의 핵심

  • app.set('port', 포트)로 서버가 실행될 포트 지정
  • app.get('주소', 라우터)로 GET 요청이 올 때 어떤 동작을 할지 지정
  • app.listen('포트', 콜백)으로 몇 번 포트에서 서버를 실행할지 지정

express 모듈 내에서 http 서버 사용 중 -> 내부적으로 http 서버 사용

서버 실행
1. node app 명령어 사용
2. localhost:3000 접속

get, post, 주소 각각 작성해서 if문이 없더라도 app에 메서드(get, post)를 붙여주는 방식으로 구분

app.set('port', process.env.PORT || 3000);

  • app.set('port', 3000);
    : 서버에 port라는 속성에 3000을 부여
    set 한 번 해놓고 나면 그 뒤로 app.get('port')로 자유롭게 가져다 쓸 수 있음
  • process.env.PORT
    : PORT 번호를 직접 지정할 수 있음(명령어 : SET PORT=80 이런식으로)

nodemon

node 대신 nodemon으로 서버를 실행할 수 있다.
=> 코드를 수정할 때마다 수동으로 on/off 하지 않아도 됨(html파일 수정은 제외)

nodemon app

6-2. express로 HTML 서빙하기

readFile 사용

6-3. 미들웨어 사용하기

모든 app 요청에 다 실행하고 싶은 코드 있음 -> 다 쓰면 중복

app.use((req, res, next) => {
  console.log('모든 요청에 실행하고 싶어요');
  next();
});

app.use : 미들웨어
인수에 next를 추가한 다음 next()를 실행 -> 다음 요청으로 넘어갈 수 있음

와일드카드

라우터를 많이 생성해줘야 하는 경우 name을 입력받아서 내용에 반영할 수도 있다.

app.get('/category/:name', (req, res) => {
  res.send('hello ${req.params.name}')
});
  • name에 무엇이 들어가냐에 따라 send()안에 들어갈 값이 변경됨
  • 와일드카드 코드가 다른 코드보다 위에 있는 경우 - 우선 처리됨

=> 때문에 와일드카드 코드는 다른 코드보다 밑에 작성해주어야 함

'*'도 비슷

때문에 이 코드는 다른 get요청 코드보다 밑에 작성되어야 함

6-4. 미들웨어 특성 이해하기

/about에서만 미들웨어를 실행시키고 싶은 경우

use안에 '/about'을 작성해주면 된다
=> next()에서 /about을 찾아감

미들웨어 여러 개

use안에 미들웨어를 여러 개 동시에 넣어줄 수도 있다.

  • next() -> 다음 미들웨어로 넘어감
    => 1요청, 2요청, 3요청 순서대로 실행

미들웨어 에러 처리

  • 에러 미들웨어는 매개변수에 err, req, res, next 모두 포함되어 있어야 함

  • 코드 작성 순서 : app.set -> 공통 미들웨어(app.use) -> 라우터(app.get, app.post) -> 에러 미들웨어

  • 없는 라우터에 요청 전송 -> 404 대신 에러 처리 미들웨어가 실행됨

404 커스터마이징 - 라우터랑 에러 미들웨어 코드 사이에 위치
(맞는 라우터 찾지 못함 -> 에러)
app.use 사용해서

한 라우터에서 send 여러번 -> 에러


app.use로 미들웨어를 사용한 경우도 불가

또한 writeHead를 사용한 경우에도 에러 처리가 된다(이미 send한 상태이기 때문)

6-5. next 활용법

Tip
json 여러개, json밑에 있는 console.log
=> 모두 실행됨(json은 응답을 보내기만 할 뿐 return되지는 않음)

Tip
res.json은 writeHead와 end의 기능을 같이 갖고 있다.

Tip
res.render도 res.send/res.sendFile처럼 응답을 보내는 것

next로 에러 처리

catch 내의 error에 인수 들어감 -> next를 통해 error 전달 한참 밑에 있는 에러 처리 미들웨어로 error 넘어감

next('route')

  • 같은 라우터('/')에 다른 미들웨어로 넘어가는게 아님
  • 다음 라우터로 일단 넘어감
    => next('route')가 실행되면 console.log('실행되나요')는 실행되지 않고 console.log('네')가 실행된다.

활용하는 경우 -> if문 사용

  • next('route')가 실행 -> app.get('/')로 연결
  • next()가 실행 -> 바로 밑에 있는 함수로 연결

6-6. morgan, bodyParser, cookieParser

morgan, cookieParser 설치

npm i morgan cookie-parser express-session

bodyParser는 요새 잘 안씀

morgan

: 콘솔창에 요청과 응답에 대한 내용을 기록하는 라우터

app.use(morgan('dev'));
  • morgan인수로 dev 대신 combined, common, short, tiny 등을 넣을 수 있다.

: 쿠키를 파싱하는 기능

app.use(cookieParser());
  • 쿠키 생성
res.cookies // { mycookie : 'test' }

res.cookie('name', encodeURIComponent(name), {
  expires : new Date(),
  httpOnly: true,
  path: '/',
})
  • 쿠키 삭제
res.clearCookie('name', encodeURIComponent(name), {
  httpOnly: true,
  path: '/',
})

서명(암호) 사용한 쿠키 : signedCookies 사용

app.use(cookieParser('password'));

req.signedCookies

body-parser

: 요청의 본문에 있는 데이터를 해석해서 req, body 객체로 만들어주는 미들웨어

최근 body-parser 는 express 안에 내장됨

  • 사용
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// urlencoded : form 파싱
// extended : true면 qs, false면 querystring -> qs가 더 좋음
  • req에서 body를 가져오는 방식
app.get('/', (req, res, next) => {
  req.body
  res.sendFile(path.join(__dirname, 'index.html'));
});

express에 들어있지 않은 bodyparser : raw(), text()
(잘 안씀)

form에서 이미지, 영상 파일 등을 처리 못함 -> multer 모듈 사용 x

6-7. static 미들웨어

: 정적인 파일들이 제공하는 라우터 역할

app.use('요청 경로', express.static('실제 경로'));

app.use('/', express.static(path.join(__dirname, 'public')));  //public 폴더 지정
  • 요청 경로와 실제 경로 다르게 작성할 수 있음
    ex) 요청 경로 : localhost:3000/zerocho.html
    실제 경로 : learn-express/public/zerocho.homl
    => 이런 식으로 작성하면 서버 구조를 파악하기 어려움 -> 보안에 도움

미들웨어도 순서 중요
morgan 미들웨어
-> static 미들웨어 => 파일search(파일 찾으면 여기서 끝)
-> (파일 못찾으면) 그 밑의 쿠키 파싱, 바디 파싱 등이 실행

static이 한참 밑에 있으면 쓰이지도 않을 cookie, body를 파싱하느라 비효율적으로 동작
_but 때로는 로그인이 필요할 시 등의 경우에 쿠키, 세션 미들웨어를 정적 파일보다 위에 놓을 때가 있음(원하는 대로 조정)

6-8. express-session 미들웨어

const session = require('express-session');
...

app.use(session())
...

app.get('/', (req, res, next) => {
  req.session.id = 'hello';
  res.sendFile( ... );

app.use로 session을 만들어 준 뒤 req.session.id로 id를 지정해줌
=> 해당 사용자의 id만 'hello'로 지정됨

session 관리용 미들웨어

app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
    secure: false,
  },
  name: 'session-cookie',
}));
  • resave : 요청이 왔을 때 수정사항이 생기지 않아도 다시 저장할지 여부
  • saveUninitialized : 세션에 저장할 내역이 없더라도 세션을 저장할지
    (resave, saveUninitialized은 주로 false)
  • cookie:httpOnly : javascript에게 공격 회피
  • name : session의 이름
  • req.session.save : 수동 저장도 가능(거의 안씀)

데이터 전송

  1. let으로 데이터 전송(위험)
let hello;

app.use((req, res, next) => {
  hello = '계좌비번'
});

app.get('/', (req, res, next) => {
  console.log(hello);
}
  • 이 방식은 hello가 전체공유가 됨 -> 보안상 위험
  1. session 사용
app.use((req, res, next) => {
  req.session.data = '계좌비번'
});

app.get('/', (req, res, next) => {
  req.session.data   // 계좌비번
}
  • 세션 요청이라 다음 요청에도 이 정보가 남아있음
  1. 한 번의 요청에만 데이터가 남게 하고 싶음 => req.data사용
app.use((req, res, next) => {
  req.data = '계좌비번'
});

app.get('/', (req, res, next) => {
  req.data   // 계좌비번
}

미들웨어 확장하기

  • 미들웨어 안에 미들웨어
app.use('/', (req, res, next) => {
  if(req.session.id) {    // 로그인함
    express.static(__dirname, 'public')(req, res, next)
  } else {                // 로그인 안함 -> next()로 넘어감
    next();
  }
});

이 방식은 로그인 한 사람들에게만 static이 실행

6-9. multer 사용하기

멀티파트 데이터 형식

form 안에 "multipart/form-data"가 있는 경우
-> 이미지나 영상 포함'

=> 요청 본문 해석 위해 multer 패키지 사용

const upload = multer({
  storage: multer.diskStorage({
    destination(req, file, done) {
      done(null, 'uploads/');
    },
    filename(req, file, done) {
      const ext = path.extname(file.originalname);
      done(null, path.basename(file.originalname, ext) + Date.now() + ext);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 },
});
  • storage : 파일 저장 위치 선택(디스크, 메모리, 클라우드 등)
    ㄴ uploads 폴더는 기존에 존재
    ㄴ filename에는 기존 이름과 수정 날짜를 수정해 준다.
  • limits : 파일 크기(fileSize)
app.get('/upload', (req, res) => {
  res.sendFile(path.join(__dirname, 'multipart.html'));
});
app.post('/upload', upload.single('image'), (req, res) => {
  console.log(req.file);
  res.send('ok');
});
  • 파일 하나만 업로드 : upload.single 사용

여러 파일을 업로드하는 경우 HTML 파일에서 multiple을 쓰면 된다.

app.post에는 single아 아닌 array 사용

app.post('/upload', upload.array('image'), (req, res) => {
  console.log(req.files);
  res.send('ok');
});

multiple이 아니라 파일 이름을 다르게 해서 여러 개 전송하는 경우

[HTML]

[app.js]

app.post에는 fields 사용

app.post('/upload', upload.fields([{ name : 'image1'}, { name : 'image2'}]), (req, res) => {
  console.log(req.files.image1);
  console.log(req.files.image2);
  res.send('ok');
});

마지막으로 image 없이 none을 사용하는 경우(이미지 없이 form일때)

app.post('/upload', upload.none(), (req, res) => {
  console.log(req.body);
  res.send('ok');
});

6-10. dotenv 사용하기

: 비밀 키 등의 환경 변수를 관리할 수 있음(미들웨어는 아님)

쿠키 키를 안전하게 관리할 때 -> env 사용

process.env.COOKIE_SECRET

이러고 .env 폴더를 생성해 줘야 한다.

그리고 app에 최대한 위에 dotenv를 불러와준다.

만약 require한 패키지 중 .env를 사용해야 하는 패키지가 있으면 dotenv를 require보다 아래에 작성해 주어야 한다.

.env도 해킹당할 위기가 있기 때문에 비밀 키도 여러 개를 만들어 직급별로 권한을 다르게 부여하는 방식으로 지킬 수 있다.

6-11. 라우터 분리하기

const router = express.Router();

=> app.js 코드가 길어지는 것 방지

app.js에 다음과 같이 작성

//...

dotenv.config();
const indexRouter = require('./routes');
const userRouter = require('./routes/user');

const app = express();
app.set('port', process.env.PORT || 3000);

app.use(morgan('dev'));

// ...

app.use('/', indexRouter);
app.use('/user', userRouter);

// ...
  • indexRouter, userRouter로 라우터를 분리해준다.
    => routes라는 폴더를 만들고 그 안에 index.js를 생성
  • 새로 만든 index.js에서 router 변수를 만든 후 app대신 사용하면 된다.
    (user도 마찬가지)

라우트 매개변수

동적으로 변하는 부분을 받을 수 있음(일반 라우터보다 뒤에 위치)

router.get('/user/:id', function(req, res) {
  console.log(req.params, req.query)
})'

이 코드에 주소 요청은 다음과 같은 식으로 할 수 있다.

/users/123?limit=5&skip=10

=> id = 123

404 미들웨어

모든 라우터에서 걸리는 것 없을 때 404 에러가 발생함

app.use((req, res, next) => {
  res.status(404).send('Not Found')'
});

모든 라우터 뒤에 만들어야 함

라우터 그룹화(잘 안씀)

주소는 같지만 메서드가 다르면 router.route로 묶을 수 있다.

  • 그룹화 전
router.get('/abc', ... );
router.post('/abc', ... );
  • 그룹화 후
router.route('abc')
	.get(...)
    .post(...)

req, res 객체 살펴보기



end부터 sendFile은 한 번씩만 써야함 -> 그렇다고 return 역할을 하는것도 아님

메서드 체이닝 지원

res
	.status(201)
	.cookie('test', 'test')
	.redirect('/admin');

6-12. pug 템플릿 엔진

템플릿 엔진
=> HTML의 정적인 단점을 개선

  • 반복문, 조건문, 변수 등을 사용할 수 있음
  • 동적인 페이지 작성 가능
  • PHP, JSP와 유사

보통은 app.set과 app.use 사이에 코드 작성

  • 확장자 지정 : view engine = pug
  • views라는 폴더를 지정
    => views 안의 파일들을 pug로 지정

Pug - HTML 표현

  • 자식 태그는 tap으로 표시
    title= title => 변수 작성
  • id, clas, span, class는 각각 #, . , span#, p.으로 작성
  • p하고 띄어쓰기 : p의 내용물
  • html의 buttom type처럼 button의 속성은 괄호() 처리

  • 텍스트 여러 줄 입력 => '|' 사용
  • stype, script로 작성 => 점(.) 사용

Pug - 변수

  • res.render
    두 번째 인수 객체에 Pug 변수
router.get('/', (req, res, next) => {
  res.render('index', { title: 'Express' })   // 여기서 title 변수는 Express가 됨
});                       
  • res.locals
router.get('/', (req, res, next) => {
  res.locals.title = 'Express';   // 여기서 title 변수는 Express가 됨
  res.render('index');
});                       

이 경우에는 res.locals객체에 다른 미들웨어에서도 접근 가능함

  • 변수 사용

    텍스트로 사용 - 태그 뒤에 '=' 붙임
    속성에 사용 - '=' 사용
    텍스트 중간 ' #{변수} 사용

Pug - 파일 내 변수

퍼그 파일 안에서 변수 선언 가능하다.

  • '-'뒤에 자바스크립트 사용

  • 자동 이스케이프(HTML로 렌더링되는 것을 막는 것)

Pug - 반복문

  • for in 이나 each in으로 반복문 돌릴 수 있음

  • 값과 인덱스 가져올 수 있음

Pug - 조건문

Pug - include

퍼그 파일에 다른 퍼그 파일

Pug - extends와 block

레이아웃을 정할 수 있음(include와도 같이 사용)

6-13. 넌적스 템플릿 엔진

  • view engine을 pug가 아닌 numjuck로 교체
  • app.set이 아니라 numjucks로 세팅

넌적스 - 변수

변수는 중괄호 2개를 사용해서 {{title}} 방식으로 넣을 수 있다.

  • 내부 선언 : {%set 변수 = '값' % }

    이스케이프 하고 싶지 않으면 '| safe'를 붙여준다.

넌적스 - 반복문

  • for in문과 endfor 사이에 반복문을 작성해 줄 수 있음
  • loop를 사용해 index를 자동으로 붙여줄 수 있음

넌적스 - 조건문

  • {% if %} 안에 조건문 작성
  • endif로 if문을 끝내주어야 함 endif로 if문을 끝내주어야 함

넌적스 - include

넌적스 - 레이아웃(block and extends)

에러 처리 미들웨어

에러 발생 시 템플릿 엔진과 상관없이 템플릿 엔진 변수 설정하고 error 템플릿을 렌더링

  • res.locals.변수명으로도 템플릿 엔진 변수 생성 가능
    process.env.NODE_ENV는 개발 환경인지 배포환경인지 구분해주는 속성
app.use((req, res, next) => {
  const error = new Error(`${req.method} ${req.url} 라우터가 없습니다.`);
  error.status = 404;
  next(error);
}

이 코드에서 404에러 발생 시 에러 처리 미들웨어에 err로 전달된다.

app.use((err, req, res, next) => {
  res.locals.message = err.message;
  res.locals.error = process.env.NODE_ENV !== 'production' ? err : {};
  res.status(err.status || 500);
  res,render('error');
});
  • res.locals.message에 err의 메시지가 대입('~~ 라우터가 없습니다.')
  • process.env.NODE_ENV !== 'production' => 배포가 아닌 경우엔 화면에 에러 메시지가 그대로 표시되고 배포인 경우에는 err : {}이 표시됨
    (에러 메시지가 배포 화면에 보이면 보안상 문제가 있음)
  • res.status : 404인지 500인지 구분

0개의 댓글