[6장] Express/multer

2022. 5. 6. 17:26책/Node.js 교과서

multer부분을 따로 뺀 이유는 사용 자체가 어려운 편에 속하고, 짚고 넘어가야 할 개념들이 많기 때문이다. 그렇다고 모른 채 지나간다면 파일 업로드를 처리할 수 없으므로 차근차근 알아보기로 하자.

1. 파일업로드의 본질

1.1 HTTP 메시지
이전에도 설명했지만 HTTP 요청/응답은 Header와 Body로 구성되어 있다. Header를 보고 '어떤 형태'의 데이터인지 확인하고 Body를 보고 구체적인 '내용'을 확인할 수 있는 것이다. 즉, 모든 HTTP 메시지는 Header와 Body가 적절한 형태로 채워져 있어야 소프트웨어가 동작할 수 있다.

1.2 클라이언트 → 서버 파일 업로드 과정 이해하기
파일 업로드를 구현한다고 해보자. 클라이언트 측에서 파일을 실어서 서버로 보낼 것이고, 서버는 이를 보고 해석한 후 필요한 응답을 해줄 것이다. 그리고 이 모든 과정 또한 HTTP 메시지를 주고받는 일에 속한다. 즉, '어떤 형태'의 데이터를 실을 것인지 Header를 명시해줘야 하고, 파일의 구체적인 '내용'을 Body에 담아줘야 한다.

  1. 클라이언트 측에선, 그러니까 웹 브라우저의 경우라면 폼 태그를 통해 파일을 등록한 후 서버로 전송한다
  2. 이 말은 HTTP 메시지를 서버로 보낸다는 뜻이고, 이에 따라 Header와 Body를 적절한 모양으로 채워줘야 한다
  3. 즉, HTTP 메시지의 Header중 Content-type을 채워줘야 하는데, 이때 multipart/form-data로 지정하게 되는 것. (더 정확하게 표현하자면, Content-type은 HTTP 메시지의 Header 중 엔티티 헤더에 속한다)
더보기

# <form> 태그

- name : 서버에 데이터를 보낼 때 데이터 구분을 위해 달아주는 이름

- action : 데이터를 보낼 경로

- method : HTTP 요청 메서드

- enctype : 데이터를 서버로 전송할 때, 해당 데이터가 인코딩되는 방식

 

# enctype

- application/x-www-form-urlencoded : default 값으로, 모든 문자들을 서버로 보내기 전에 인코딩 됨을 명시한다
- text/plain : 공백 문자는 '+'기호로 변환하되 나머지 문자는 모두 인코딩되지 않음을 명시
- multipart/form-data : 모든 문자를 인코딩하지 않는다. 보통 파일이나 이미지를 서버로 전송할 때 사용한다

2. Multipart 탄생 배경

그런데 왜 굳이 Content-type으로 multipart가 필요했던 걸까? 그 이유는 한 번에 여러 타입을 처리하기 위함이다.

위 사진에서 폼 태그의 <input> 태그를 보자. 현재 하나의 폼 안에서 'file' 타입을 받는 <input>, 'text'타입을 받는 <input> 각각 한 개씩 존재한다. 즉 각 요청의 content-type은 'image/jpeg', 'text/palin'인 것이다.

보통 HTTP Request의 Content-type은 한 종류로 지정되나 위와 같은 상황에선 타입이 다른 두 데이터가 하나의 폼안에 담겨서 서버로 보내진 다는 것이다. 따라서 둘을 적절하게 구분해주면서 한 번에 전송 및 처리될 수 있도록 해주는 것이 multipart 타입인 것이다.

그리고 이 multipart 타입을 처리해주는 미들웨어가 multer인 것.

3. multer

multipart/form-data를 다루기 위한 미들웨어 (Multer is a node.js middleware for handling multipart/form-data, which is primarily used for uploading files. It is written on top of busboy for maximum efficiency)

 

req.file(혹은 req.files)과 req.body 객체를 생성해 서버로 업로드된 파일을 핸들링할 수 있게 해 준다. 파일을 전달받는 개수에 따라 호출하는 메서드가 약간씩 달라진다. 아래는 멀터객체 생성 및 메서드들.

 

더보기

# multer( options )
멀터 객체를 반환한다. body와 file객체를 (업로드된 파일이 복수개일 경우 files) request 객체에 추가하고, 이를 통해 파일 업로드를 제어한다. 

 - options : 보통의 웹 앱에선 파일 업로드 위치를 지정하는 dest (혹은 storage) 옵션 정도로도 충분하다

dest(혹은 storage) 파일이 저장될 위치
fileFilter 어떤 파일을 허용할지 제어하는 함수
limits 업로드 데이터 한도
preservePath 파일의 base name대신 보존할 파일의 전체 경로


# multer.single( fieldname )
fieldname 인자에 명시된 한 개의 파일을 전달받는다. 전달받은 파일은 req.file에 저장.
( fieldname : 폼 태그의 <input name="어쩌구" >에서 지정해준 name을 말함)

# multer.array( fieldname [, maxCount] )
fieldname 인자에 명시된 여러 개의 파일을 전달받는다. 전달받은 파일은 req.files에 저장.

# multer.fileds( fields )
fieldname이 서로 다른 파일들을 전달받을 때 사용한다. 전달받고자 하는 필드네임을 배열 형태로 지정해주면 해당 필드네임에 맞는 파일들을 가져올 수 있음.
ex) const fields = [ { name: 'image1' }, { name: 'imageSet', maxCount: 4 } ];


##### storage engines #####
멀터에 storage 옵션을 지정할 때 storage의 설정을 지정해주는 컴포넌트. 파일을 '디스크'에 저장하도록 도와주는 엔진인 diskStorage와 '메모리'에 저장하도록 도와주는 'memoryStorage' 엔진이 있다. 그 외 필요한 것들은 서드파티 라이브러리로 추가할 수 있다.

# multer.diskStorage( [destination] [, filename] )

  • destination <string>|(req, file, callback)
    어떤 폴더에 파일을 저장할지 결정. 따로 지정해주지 않으면 os에서 임시 파일을 저장하는 기본 디렉토리를 사용한다. 경로를 직접 'string' 형태로 지정해주거나 callback함수를 넣어서 적당히 처리 후 (필요하다면 에러처리도 가능) 경로를 지정해줄 수 있다.
    - callback(error :Error, destination :string) : 에러가 있다면 에러를 넣어주고, 나머진 파일을 저장할 경로를 'string' 타입으로 지정해준다
  • filename(req, file, callback)
    업로드된 파일을 저장할 때 따로 파일명을 지정하는 옵션. 확장자도 함께 명시해줘야 한다.
    - callback(error :Error, filename :string)

 

3.1 단일 파일 업로드

위 그림처럼 하나의 파일을 업로드한다고 치자. 지금은 익숙지 않아 어디서부터 손대야 할지 감도 안 잡히겠지만 아래와 같은 과정을 거친다고 생각하고 코드를 짜면 된다.

 

  1. 클라이언트로부터 파일을 전달받을 수 있게 폼 양식을 구성한다
  2. multer 객체를 생성해 전달받은 파일을 어떻게 요리할지 구성한다
  3. 라우터에 만들어준 multer 미들웨어 함수를 single 메서드로 감싸서 껴넣는다. 끝.
더보기
import multer from '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);
        }
    })
})

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

코드설명
path.extname( '파일명' ) : 파일의 확장자를 추출한다 ('.js'같은)
path.basename('파일명', '확장자') : 파일명에서 확장자를 제거한 나머지 값을 반환

file.originalname : 클라이언트측에서 업로드한 파일명

참고로 multer( ... ) 부분에서 콜백함수로 받는 file은 아래처럼 생겼음
file
{
  fieldname: 'image',
  originalname: '그림1.png',
  encoding: '7bit',
  mimetype: 'image/png'
}


콘솔창
// req.file
{
  fieldname: 'image',
  originalname: '그림1.png',
  encoding: '7bit',
  mimetype: 'image/png',
  destination: 'uploads/',
  filename: '그림11651848968029.png',
  path: 'uploads\\그림11651848968029.png',
  size: 97563
}

// req.body
{ title: '그림1 업로드' }

 

3.2 복수 파일 업로드(array)

업로드 파일의 개수가 늘어났다고 드라마틱하게 뭐가 변하지는 않는다. 다만 multer 객체를 미들웨어에 껴넣을 때 array( ) 메서드로 감싸고 req.files로 읽는다는 정도만 달라질 뿐.

더보기

 

 

// 이 부분은 달라질 게 없어서 생략했다
const upload = multer( ... ) 

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

 

3.3 복수 파일 업로드(fields)

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

 

4. 정리

결국 멀터로 파일을 업로드할 땐 아래 세 단계를 거친다고 생각하면 된다.

 

  1. 클라이언트로부터 파일을 전달받을 수 있게 폼 양식을 구성한다
  2. multer 객체를 생성해 전달받은 파일을 어떻게 요리할지 구성한다
  3. 라우터에 만들어준 multer 미들웨어 함수를 업로드 파일 갯수에 맞게 메서드로 잘 감싸서 껴넣는다. 끝.

' > Node.js 교과서' 카테고리의 다른 글

[7장] MySQL  (0) 2022.05.07
[6장] Express / Router  (0) 2022.05.07
[6장] Express  (0) 2022.05.05
[5장] npm 패키지 매니저  (0) 2022.05.04
[4장] 서버 만들기 (cluster)  (0) 2022.05.03