[Express]핵심개념

Updated:

Express 핵심개념

이 게시글은 Express의 핵심인 미들웨어부터 그외 추가적인 기능까지 알아본다

1. 기본 미들웨어

-> 요청과 응답의 중간에 위치하는 것을 미들웨어라고 부르고, app.use와 함께 사용한다
-> 기본적으로, Node.js의 Express는 위에서부터 코드를 읽는다는 사실 잊지말자!!!

app.use()


const express = require('express');
const path = require('path');

const app = express();

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

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

app.get('/', (req, res)=>{
  res.sendFile(path.join(__dirname, 'index.html'));
});

app.get('/about', (req, res)=>{
  res.send('Welcome to Express server');
});

app.listen(app.get('port'), ()=>{
  console.log('express서버가 3000포트에서 실행중~');
});

-> 여기서 app.use안에 들어가는 함수가 미들웨어라고 생각하면 되고, 각각의 ‘app.’으로 구성된 코드를 라우터라고 부른다
-> app.use()를 이용하여 모든 요청에서 실행되는 코드를 추출할 수 있다
-> 인자로 실행할 함수가 들어가는데, 함수의 인자로는 기존에 쓰던 request, response 이외에도 다음에 실행할 함수로 next라는 인자가 들어간다. 예를 들어 ‘localhost:3000/’으로 접근했을시, app.use()실행 후에 app.get(‘/’)를 실행하기 위해서는 next() 작성이 요구된다

라우트 매개변수


const express = require('express');
const path = require('path');

const app = express();

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

app.get('/', (req, res)=>{
  res.sendFile(path.join(__dirname, 'index.html'));
});

app.get('/category/:name', (req, res)=>{
  res.send(`welcome to ${req.params.name}`);
})

app.get('/category/js', (req, res)=>{
  console.log(req.query);
  res.send('Welcome to javascript');
});

app.listen(app.get('port'), ()=>{
  console.log('express서버가 3000포트에서 실행중~');
});

-> 위의 예시에서 파라미터를 ‘/:name’ 으로 설정해서 name이라는 변수에 여러가지 값을 담을 수 있다. 예를들어 ‘localhost:3000/category/java’로 접근할 경우, ‘welcome to java’가 화면에 뜬다. ‘localhost:3000/category/html’로 접근할 경우, ‘welcome to html’이 뜨게 된다.
-> 위의 코드에서 ‘localhost:3000/category/js’로 접근할 경우, 아래의 get()과 겹치는데, 위에서부터 코드를 읽기 때문에, 상대적으로 위에 있는 코드만 실행된다. 때문에, 라우트 매개변수는 맨아래에서 사용해주는 것이 좋다
-> ‘req.params’는 name을 key값으로 가지는 객체를 반환한다. 굳이 name이 아니라 다른 값으로 적어도 된다
-> ‘req.query’는 파라미터 뒤에 받은 쿼리속성값들을 key로 하는 객체를 반환한다

에러처리 미들웨어


-> Express에서 에러를 직접 처리해주지만, 모든 에러를 직접 보여주면 보안상의 문제가 발생할 수 있기 때문에, 개발자가 직접 에러 미들웨어를 만들어주는 것이 권장된다

const express = require('express');
const path = require('path');

const app = express();

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

app.get('/', (req, res)=>{
  res.sendFile(path.join(__dirname, 'index.html'));
}, (req, res)=>{
  throw new Error('에러');
});

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

app.use((err, req, res, next)=>{
  console.error(err);
  res.status(500).send(err.message);
});

app.listen(app.get('port'), ()=>{
  console.log('express서버가 3000포트에서 실행중~');
});

-> app.use의 함수를 이용하면 에러를 처리하는 미들웨어를 만들 수 있는데, 여기서 주의할 점은 반드시 error, request, response, next 4개의 인자를 모두 적어주어야 작동한다는 점이다
-> 위 예시에서 app.get(‘/’) 로 던져진 에러를 4개의 인자를 가지는 app.use 에러처리 미들웨어에서 받아 처리한다. 여기서 status(500)으로 설정했으므로, HTTP 상태코드는 500으로 뜰 것이다
-> 추가적으로, app.use를 경로 처리 라우터보다 아래에 놓으면 상위의 라우터에 해당되지 않는 경로요청이 들어올 경우를 처리한다

response 상세


-> Express를 사용하면, 기존 Node.js에서 복잡하게 작성하던 코드를 간단히 작성할 수 있다

// 기존코드
app.get('/', (req, res)=>{
  res.writeHead(200, {'Content-Type':'text/plain;charset=utf-8'});
  res.end('Welcome to Express server');
});

// Express코드
app.get('/', (req, res)=>{
  res.status(200).send('Welcome to Express server');	// status(200) 생략가능
});

-> 위쪽이 기존 Node.js에서 사용하던 코드이고, 아래가 Express를 사용하여 간단히 작성한 코드이다

// 기존코드
app.get('/json', (req, res)=>{
  res.writeHead(200, {'Content-Type':'text/plain;charset=utf-8'});
  res.end(JSON.stringify({name:'saemyung'}));
});

// Express코드
app.get('/json', (req, res)=>{
  res.json({name:'saemyung'});
});

-> json을 사용할 경우에도 아래와 같이 간단한 코드작성이 가능해졌다

app.get('/', (req, res)=>{
  res.send('Welcome to Express server');
  res.sendFile(path.join(__dirname, 'index.html'));
  res.json({name:'saemyung'});
});

-> 위는 세번의 send를 하기 때문에 ‘Cannot set headers after they are sent to the client’의 오류가 발생하는 코드이다

next()


-> next는 파라미터에 해당되는 다음 미들웨어/라우터를 실행하는 역할을 한다

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

app.get('/', (req, res)=>{
  res.send('인덱스 페이지입니다.');
});

app.get('/about', (req, res)=>{
  res.send('Welcome to Express server');
});

-> ‘/about’에 접근할 경우, 먼저 app.use가 실행되고 next()에 의해 ‘/about’에 해당하는 다음 미들웨어인 app.get(‘/about’)을 실행한다

app.use((req, res, next)=>{
  try{
    console.log(a);		// 여기서 a는 정의되지 않은 변수(에러 발생)
  } catch(err){
    next(err);
  }
});

-> 원래 next안에 아무런 인자값을 넣지 않을 경우, 다음 미들웨어가 실행된다. 하지만, next안에 err인자값을 넣을 경우, 다음으로 에러처리 미들웨어가 실행된다

app.get('/', (req, res, next)=>{
  if(true) next('route');
  else next();
}, (req, res)=>{
  console.log('거짓일 경우 실행');
});

app.get('/', (req, res)=>{
  console.log('참일 경우 실행');
});

-> next(‘route’)를 이용하면, 다음 라우터를 실행하도록 작동한다. 예를 들어 if문에 의해 true일 경우, next(‘route’)에 의해 다음 라우터인 app.get(‘/’)이 실행되고 false일 경우, next()에 의해 다음 미들웨어인 ‘ (req, res)=>{ console.log(‘거짓일 경우 실행’)}; ‘이 작동한다

미들웨어간 data전송


app.use((req, res, next)=>{
  res.locals.data = 'password';
  next();
});

app.get('/', (req, res)=>{
  console.log(res.locals.data);	// password
});

-> ‘res.locals.data’ 를 통해 미들웨어간의 data전송이 가능하다. 이 data는 현재 요청동안만 남아있고 새로운 요청이 오면 res.locals는 초기화된다. 그렇기 때문에 보안관련 문제는 걱정하지 않아도 된다

미들웨어 기능 확장


app.use('/', express.static(__dirname, 'public'));
app.use('/', (req, res, next)=>{
  express.static(__dirname, 'public')(req, res, next);
});

-> 위 2가지 코드는 같은 기능을 하는 코드이다. 다만, 아래처럼 하면 더 확장성 있는 코드작성이 가능하다

app.use('/', (req, res, next)=>{
  if(req.session.id){	// session id가 존재할 경우
    express.static(__dirname, 'public')(req, res, next);
  } else{
    next();
  }
});

-> 미들웨어 기능 확장을 이용하여 분기문 처리도 가능하다

2. 추가적인 미들웨어


// 기존 Cookie parser
const parseCookies = (cookie = '')=>{
  return cookie
    .split(';')
    .map(e => e.split('='))
    .reduce((acc, [k,v])=>{
    		acc[k.trim()] = decodeURIComponent(v);
    		return acc;
  }, {});
};
// 기존 Cookie응답
res.writeHead(302, {
  Location: '/',
  'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});

// Express에서의 Cookie parser와 Cookie응답
const cookieParser = require('cookie-parser');
app.use(cookieParser('비밀키'));	// 인자값으로 넣은 비밀키를 통해 해당쿠키가 내서버에서 만든 쿠키임을 증명가능

app.get('/', (req, res)=>{
  console.log(req.cookies);		// {myCookie: 'saemyung'}
  res.cookie('name', encodeURIComponent(name), {
    expires: Date.now(),
    httpOnly: true,
    path: '/',
  })
});

-> Express에서 제공하는 cookieParser를 require로 가져오면 req.cookies로 쿠키를 가져오는 것이 가능하고, res.cookie() 를 통해 Cookie응답을 처리할 수 있다

Body Parser


// 기존 Body Parser
const key = req.url.split('/')[2];
let body = '';
req.on('data', (data)=>{
  body += data;
});

return req.on('end', ()=>{
  console.log('PUT 본문:', body);
  users[key] = JSON.parse(body).name;
  res.writeHead(200, {'Content-Type': 'text/plain; charset=utf-8'});
  return res.end(JSON.stringify(users));
});

// Express에서의 Body Parser
app.use(express.json());		// json파싱을 위한 설정
app.use(express.urlencoded({extended: true}));		// HTML form태그에서 받는 data파싱을 위한 설정

app.get('/', (req, res)=>{
  console.log(req.body.(파라미터명);
});

-> 클라이언트 측에서 json을 보내는 경우와 HTML의 form태그에서 데이터를 보내는 경우 모두 2개의 app.use 코드를 통해 간단한 처리가 가능하다
-> ‘req.body.(파라미터명)’을 통해 해당 데이터를 가져올 수 있다

Session


const session = require('express-session');
app.use(session({
  resave: false,	// 요청이 올때 세션에 수정사항 없더라도 세션을 다시 저장할지 여부
  saveUninitialized: false,	// 세션에 저장할 내역이 없더라도 처음부터 세션 생성할지 여부
  secret: '비밀번호',
  cookie: {
    httpOnly: true,
  },
  name: 'connect-sid',
}));

dotenv


-> cookieParser이용시의 인자값과 session이용시 secret값으로 직접 비밀키를 입력한다면, 해당 소스코드가 해커에게 보여졌을 때에 그대로 유출되기 때문에 이를 방지하기 위해 사용하는 것이 dotenv이다

// dotenv 쓰기 위한 설정
const dotenv = require('dotenv');
dotenv.config();

// cookieParser 이용시 보안처리
app.use(cookieParser(process.env.COOKIE_SECRET));

// session 이용시 보안처리
app.use(session({
  resave: false,
  saveUninitialized: false,
  secret: process.env.COOKIE_SECRET,
  cookie: {
    httpOnly: true,
  },
  name: 'connect-sid',
}));

-> ‘process.env.COOKIE_SECRET’에 값을 넣어주기 위해서는 ‘.env’파일을 만든 후에 해당 파일에 ‘COOKIE_SECRET’의 이름으로 값을 넣어주면 되고, 이때 세미콜론은 붙이지 않는다
-> 여기서 만든 ‘.env’ 파일은 절대 GitHub같이 공유하는 곳에 올리면 안되고 팀원들끼리만 직책에 따라 공유해야 한다

static 미들웨어


-> 특정 작업 처리를 위한 경로를 제외한 HTML, CSS, JS 파일접근 등 나머지 경로를 처리하기 위해 필요한 것이 static 미들웨어이다

// Node.js의 코드
if(req.method === 'GET'){
  if(req.url === '/'){
    const data = await fs.readFile('./4장/2/restFront.html');
    res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
    return res.end(data);
  } 
// 기존 static 처리
try{
  const data = await fs.readFile(path.join(__dirname, req.url));
  return res.end(data);
} catch(err){
}
}

// Express의 static 미들웨어
app.use('요청 경로', express.static(path.join((__dirname, '실제 경로')));

-> 기존 Node.js에서 try-catch문을 이용하여 static처리하던 것을 express.static 키워드를 사용하여 처리한다
-> 예를 들어 ‘app.use(‘/’, express.static(path.join(__dirname, ‘public’)))’으로 코드를 작성하면 클라이언트 측에서 ‘localhost:8080/index.html’로 요청할 경우 ‘public/index.html’을 찾아서 불러온다고 생각하면 된다
-> 여러가지 app.use를 사용할 경우, 이 static의 app.use를 어디에 놓는지에 따라 효율적인 코드작성이 가능하다. 예를 들어, static 미들웨어 작동시에 cookie, body parser등이 필요하지 않다면 해당 app.use 코드보다 위에 static 미들웨어에 관한 app.use를 적는 것이 효율적일 것이다

multer


-> 이미지, 동영상 등 여러가지 파일을 멀티파트 형식으로 업로드할 때 사용하는 미들웨어

<form action='/upload' method='post' enctype='multipart/form-data'>
  <input type='file' name='image' />
  <input type='text' name='title' />
  <button type='submit'>
    업로드
  </button>
</form>

-> 위와 같이 enctype이 ‘multipart/form-data’인 form을 통해 업로드하는 형식을 멀티파트 형식이라 부름
-> 멀티파트 형식은 body-parser로는 처리불가하기 때문에 multer라는 미들웨어를 따로 사용함

const multer = require('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},
});

-> 일단, multer는 storage와 limits속성을 지니는 객체를 인자로 갖는다. 그 중 storage는 어디에(destination) 어떤 이름(filename)으로 저장할지에 관한 정보가 들어있다
-> destination과 filename은 각각 req, file, done 3개의 매개변수를 가지며 req에는 요청에 대한 정보가, file에는 업로드한 파일에 대한 정보가 들어있고, done은 작업을 처리할 함수이다. done의 첫번째 인자값에는 에러가 있을 경우 넣고 없는 경우 null을 넣는다. 두번째 인자값으로는 실제 경로나 파일이름을 넣어준다
-> limits를 통해 업로드할 파일사이즈를 제한한다

const fs = require('fs');

try{
    fs.readdirSync('uploads');
} catch(err){
    console.err('uploads명의 폴더가 존재하지 않아서 생성함');
    fs.mkdirSync('uploads');
}

-> 이전의 코드에서 ‘uploads’ 폴더가 없을경우 오류가 발생하기 때문에 위의 코드를 이용하여 서버 시작시에 ‘uploads’ 폴더를 생성한다

<form action='/upload' method='post' enctype='multipart/form-data'>
  <input type='file' name='image' />
  <button type='submit'>
    업로드
  </button>
</form>
app.post('/upload', upload.single('image'), (req, res)=>{
    console.log(req.file);
    res.send("업로드 성공");
});

-> 이전에서 생성한 upload 객체를 필요한 라우터에서 가져다가 쓸 수 있다
-> single버전은 파일을 하나만 업로드할 때 사용한다
-> 여기서 ‘upload.single()’의 인자값으로 들어가는 것은 html파일의 form input태그의 name속성과 같아야 한다
-> 업로드한 파일정보를 ‘req.file’ 에 넣어준다

<form action='/upload' method='post' enctype='multipart/form-data'>
  <input type='file' name='image' multiple />
  <button type='submit'>
    업로드
  </button>
</form>
app.post('/upload', upload.array('image'), (req, res)=>{
    console.log(req.files);
    res.send("업로드 성공");
});

-> 여러가지 파일을 업로드할 경우에는 html에 multiple을 적어주고, ‘upload.array()’를 사용한다
-> 마찬가지로 ‘upload.array()’의 인자값으로 들어가는 것은 html파일의 form input태그의 name속성과 같아야 한다
-> 업로드한 파일정보들은 ‘req.files’ 에 배열형태로 들어있다

<form action='/upload' method='post' enctype='multipart/form-data'>
  <input type='file' name='image1' />
  <input type='file' name='image2' />
  <button type='submit'>
    업로드
  </button>
</form>
app.post('/upload', upload.fields([{name: 'image1'}, {name: 'image2'}]), (req, res)=>{
    console.log(req.files.image1);
    console.log(req.files.image2);
    res.send("업로드 성공");
});

-> 마찬가지로 여러가지 파일을 업로드하는데 name값이 다른 경우에는 ‘upload.fields()’를 사용한다
-> ‘upload.fields()’의 인자값으로 들어가는 것은 html파일 input의 name속성값을 value로 가지는 객체들의 배열이다
-> 업로드한 파일정보들은 ‘req.files.(name속성값)’을 통해 접근가능하다

3. 그외 추가적인 기능들

라우터 분리하기


-> 하나의 파일에 모든 라우터를 작성하면 코드가 지저분해질수 있기 때문에 Express에서는 라우터를 분리할 수 있는 기능을 제공한다

const express = require('express');
const router = express.Router();

router.get('/divide', (req, res)=>{
  res.send('This is divided router');
});

module.exports = router;

-> 현재폴더에 route라는 임의 명칭의 폴더를 만든후 out.js라는 임의의 명칭으로 위 파일을 저장한다

const dividedRouter = require('./route/out');

app.use('/inside', dividedRouter);

...

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

-> 기존 app.js 메인파일에 위와 같이 코드를 작성하면, 라우터 분리가 완료되었다. 이때 클라이언트의 접근주소는 2개 파일의 경로를 합한 것이 된다. 위 예시에서는 ‘localhost:3000/inside/divide’를 통해 접근가능하고, 만약 반대로 ‘localhost:3000/divide/inside’로 접근시에는 404 오류가 발생한다
-> 만약, route폴더에 index.js의 명칭의 파일을 사용할 경우 require로 가져올때 index는 생략이 가능하다

라우터 그룹화


-> ‘get’과 ‘post’ 요청에서 같은 경로를 사용할 경우 그룹화하여 사용할 수 있다

app.route('/common')
    .get((req, res)=>{
        res.send('This is GET method');
    })
    .post((res, req)=>{
        res.send('This is POST method');
    });

-> 위와 같이 ‘get’, ‘post’ 모두 ‘/common’의 경로에서 사용할 경우에 route()를 이용해 한번에 묶어 사용할 수 있다

Leave a comment