Search
▪️

File Upload & Download

Handling Multipart Form Data

아무런 조치 없이 File에 대해서 console.log()를 찍으려고 하면, Console 창에는 아무것도 보이지 않는다. 이는 Input의 File에 대해서 정확히 파일을 Extract하지 못했기 때문이다.
Request로 부터 Content를 Extract하지 못하는 이유는 다음과 같다. 모든 Incoming Request에 대해서 Extract되는 Content들은 app.js의 Middleware에서 Body Parser로 URL Encoded Parser로 설정하였기 때문에 오로지 Text들 (Number, URL, Plain Text)에 대해서만 처리할 수 있다. 즉, File에 대해서는 처리하지 못하는 Parser를 사용하고 있기 때문이다.
URL Encoded Parser의 경우, Form에서 입력 받은 모든 데이터 (Number, URL, Plain Text)들을 Text로 Encode하여 제출하게 된다. 이렇게 Encode된 것을 URL Encoded라고 한다. 이런 Content들의 Type은 x-www-form-urlencoded이다.
이렇듯 File에 대해서는 Content를 Extract하는데 실패하기 때문에 (File은 Text가 아닌 Binary이므로), 새로운 Package를 통해서 File에 대해서 Content를 Extract할 수 있어야 한다. (기존에 설치하여 설정한 Body Parser는 File에 대해서 Handling 할 수 없기 때문이다.) 아래 명령어를 통해서 File에 대해서 처리할 수 있는 Parser를 설치할 수 있다.
npm install —save multer
Multer는 Incming Reqest에 대해서 FIle에 대해서 Parsing을 지원한다. (File만이 아니라 Text에 대해서도 처리 가능하다.)
FIle에 대한 Parsing이 필요한 Frontend의 Form에서 enctype="multipart/form-data"를 할당한다. (기본은 application/x-www-form-urlencoded 이다.) 이를 통해서 Server에게 이 Request는 Plain Text 뿐 아니라 File도 들어간 Mixed Data (Text와 Binary를 모두 처리해야 한다는 것을) 임을 알린다.
이전에 설치했던 Multer라는 Third Party Package는 multipart/form-data Type에 대해서 Parsing을 처리한다. 즉, Text와 File에 대해서 Parsing을 한다.

Handling File Uploads with Multer

Multer로 File에 대해서 Parsing을 수행하기 위해선, Body Parser처럼 Multer도 Middleware에 두어 모든 Incoming Request에 대해서 처리할 수 있도록 한다. 이렇게 했을 때, multipart/form-data에 대해서도 Request에 담긴 내용을 볼 수 있다.
Multer를 Import했으면, app.js에서 app.use()를 통해서 multer() Method를 실행할 수 있다. 여기서 끝이 아니라 Chaining을 통해서 붙어야 하는 Method가 있다. Multiple File을 받을 것인지 Single File을 받을 것인지 설정하는 것이다. 후자의 경우 single() Method를 이용하고, 인자는 Input File의 이름을 인자로 준다.
app.use(multer().single('$name'))
Multer를 사용하기 위해 multer() Method를 실행할 때, Object를 인자로 줄 수 있다. Object에는 Option Key와 Value를 준다. 예를 들어서 dest 옵션을 줬을 때, Request File의 Buffer에 있는 데이터들을 Binary 데이터로 되돌려서 dest로 준 Path에 저장하게 된다. (저장된 File의 이름은 Random Hashed Value로 저장되며, 확장자 이름도 존재하지 않고, Image로 인식되지도 않는다. 하지만 여기에 확장자 명을 강제로 붙여보면 Image로 보이는 것을 확인할 수 있다. 즉, Option으로 준 dest외에도 추가 설정이 필요한 것을 알 수 있다.)
위와 같이 처리했다면, File을 Input으로 사용 했을 때, console.log()를 찍어보면 request.body가 아닌 request.file에 고스란히 multipart/form-data가 담겨 있는 것을 확인할 수 있다.
즉, File들은 Multer에 의해서 Request Object의 Body가 아닌, Request의 File에 저장되어 처리되는 것을 알 수 있다.
Request File에는 Input Form에서 사용했던 Field Name, File Name, Mime Type, Buffer에 대해서 갖고 있다. 이 Buffer에 담긴 데이터들은 이미 Stream이 된 데이터들이다. 즉, 이미 Stream된 데이터들이란 것은, Buffer는 Stream된 데이터의 결과라는 것이다. (Buffer에 이미 Stream된 데이터를 담긴 것을 확인할 수 있으므로, Server에는 이미 데이터들이 Stream되었다는 것을 의미한다.)
Request File에 저장되어 있는 Buffer는 실제 데이터들이므로 File로 되돌리는 것 역시 가능하다.

Configuring Multer to Adjust Filename & Filepath

multer() Method의 인자인 Object를 통해서 Multer의 설정 값을 줄 수 있었다. 여기서 dest외에도 storage라는 Key를 통해서 File 저장에 대한 더 많은 설정을 할 수 있다. (storage Key를 쓴다면 굳이 dest Key를 쓰지 않아도 무방하다.) storage의 Value 값을 설정을 할 때는 multer.diskStorage()를 담은 상수를 이용한다.
multer.diskStorage()의 경우 Multer를 통해서 Storage Engine에 접근할 수 있는 Method이다. 이 Method의 인자로 Object를 할당할 수 있다. Object의 Key로는 destination과 filename을 줄 수 있다.
destination의 Value는 Arrow Function을 갖는다. 이 함수는 Request와 File Object, Callback Function을 인자로 받는다. Callback Function은 Destination의 설정이 끝나면 호출되도록 한다. Callback Function의 첫 번째 인자는 Error에 대한 것이다. 해당 인자가 null이면 Error가 없는 것으로 인식한다. 이에 따라 Multer는 File을 저장해도 된다고 인식한다. 두 번째 인자는 어느 Directory에 저장할지 Path를 받는다.
filename도 destination과 마찬가지로 Arrow Function을 Value로 받는다. destination처럼 Request와 File Object, 그리고 Callback Function을 인자로 받는다. Callback Function도 Destination에서의 Callback Function과 마찬가지로 첫 번째 인자가 Error에 대한 것, 두 번째 인자가 File의 이름에 대한 정의를 돕는다.

Filtering Files by Mimetype

특정 File Type만 받기 위해선 Filtering이 필요하다. Mimetype을 Filtering함으로써 파일을 확장자 별로 골라 받을 수 있다.
multer() Method를 이용하던 Middleware의 multer() Method의 인자로 storage만 주는 것이 아니라, fileFilter라는 Key를 추가로 설정한다. fileFilter 역시 Request와 File Object 그리고 Callback Function을 인자로 갖는 Arrow Function을 Value로 사용한다.
Callback Function의 경우 storage의 destination, filename의 Callback Function처럼, 첫 번째 인자로 Error 유무, 두 번째 인자로 특정 확장자를 저장할 것이면 true, 그렇지 않으면 false를 할당한다. 이용하려고 하거나, 이용하려고 하지 않는 파일 확장자에 대해서 Callback Function을 통해서 설정한다.
File Type에 대한 비교는 file.mimetype === 'image/$fileType' 과 같이 비교한다.

Serving Images Statically

expres.static()을 통해서 Static 폴더를 지정하는 Middleware는 여럿 추가할 수 있다. Middleware를 하나 더 추가하여 CSS를 이용하고 있는 폴더 외에, Image들이 저장되는 폴더 역시 Static하게 접근할 수 있도록 해야한다. (파일 자체를 읽어와야 하므로)
Static으로 지정한 폴더는 Express.js에서 Root처럼 인식한다. 예를 들어서 images를 static으로 지정하고, images/image라고 하는 파일이 있다면, 해당 파일은 /images/image로 읽는 것이 아닌, /image로 읽어온다. 따라서 이에 유의하여 Static Middleware를 둔다. (use() Method의 Route Filtering을 이용하면 편리하다.)
File의 Path를 이용하여 HTML에서 src의 Path를 할당할 때, 기본적으로 그냥 뒀을 때 Relative Path를 이용하게 된다. 이러면 src의 Path가 원치 않는 경로로 될 가능성이 높아서 Absolute Path를 이용하는 것이 좋다.
HTML의 src Path를 Absolute Path로 만드는 방법은 Path의 제일 앞에 /를 붙이면서 Path를 할당하게 되면 Absolute Path가 된다. (/를 붙임으로써 File의 Path가 Current Path에 덧 붙여지지 않도록 한다.)

Downloading Files with Authentication

상품 List에 표현되는 상품 이미지와 같이 Public 요소들은 Static으로 지정하여 Public하게 접근할 수 있도록 해도 되지만, 그렇지 않은 File들을 Public으로 두는 것은 매우 위험한 것이다.
File을 다운로드 받는 것은 Node.js의 File System Library를 이용하여 가능하다.
fs.readFile() Method를 통해서 File을 읽을 수 있으며, Path를 준 다음 해당 File을 모두 읽었을 때 실행할 Callback Function을 할당한다. Callback Function의 인자는 Error와 Content이다.
이렇게 받으면, File이름도 이상할 뿐더러 특정 파일 확장자가 존재 하지 않는다. 이에 대해서 작업이 필요하다.

Setting File Type Header

위에서 겪은 문제대로, File 이름 및 타입을 결정하기 위해선 Header로 접근하여 설정해야 한다.
response.setHeader()를 이용해야 하며, 타입을 정하는 인자로 'Content-Type'을 줘야 하고, pdf로 확장자를 정하고 싶다면 'application/pdf'를 주면 된다. (만일 Web에서 PDF로 열리지 않고, 자동으로 File이 다운로드 된다면 캐시 및 쿠키를 지우고 다시 진행하면 된다.)
Content-Type이 아닌 Content-Disposition을 통해서 해당 Response가 Client에게 어떻게 Serve되어야 하는지도 (열리는 방식, File 이름 등)정할 수 있다. 'inline'으로 설정 값을 주면 Web에서 열리게 된다. 또한 inline뒤에 ;을 붙이고 filename을 Assign할 수 있다. 아래와 같이 말이다.
response.setHeader('Content-Disposition', 'inline; filename="$filename"')
Content-Disposition의 경우 inline이 아니라 attachment로 주면 자동으로 File 다운로드를 받을 수 있다.

Restricting File Acccess

File에 대한 Setting 및 다운로드가 가능하게 되었지만, 이를 Authentication에 따라서 수행할 수 있도록 해야 한다.

Streaming Data vs Preloading Data

fs.readFile() Method처럼 이용 시, 일부분을 읽고 Return하는 방식이 아니라 전체를 모두 다 읽고 Memory에 둔 다음 Return하는 방식이다.
이렇게 했을 때, 개발자가 고민을 해야 하는 부분이 있다. 만일 보내는 File이 굉장히 고용량이고, 이를 작업함과 동시에 많은 Requests가 Incoming되는 상황이라면, Memory가 감당하지 못하고 Overflow가 될 가능성이 높다.
따라서 Response를 단순히 File을 Serving하는 것에 대해서는 그닥 바람직한 방법은 아니다. 그러므로 사용하는 것이 Response 데이터를 Streaming하는 것이다.
사용 방법은 다음과 같다.
fs.createReadStream($path)를 통해서 path에 위치 해있는 File을 Readable Stream으로 받아온다.
이런 File을 Response에 담을 것이기 때문에 Response의 Header를 아까와 같이 Content-Type, Content-Disposition 설정을 한다.
Stream 생성 및 Response에 대한 설정을 했다면, 해당 Readable Stream을 Response에 밀어 넣어 준다.
Response에 넣는 작업은 pipe() Method를 통해서 이용하게 된다. 해당 Method의 인자는 Response가 되겠다. (pipe()는 데이터 Forwarding 작업을 수행한다.)
pipe() Method를 통해서 Response에 데이터를 Forwarding할 수 있는 이유는, Response 역시 일종의 Writable Stream이기 때문이다.
즉, Response를 받아서 뿌려주는 Browser에서는 다른 Browser Page로부터 데이터를 차근 차근 Chunk 단위로 읽어오게 되므로, 미리 Preloading을 하여 모든 데이터를 Memory에 넣을 필요가 사라지게 된다.
Stream을 이용할 시, Buffer를 이용하여 데이터를 Chunk 단위로 읽어와서 모든 Chunk가 도착할 때까지 기다리는 것이 아니라 하나씩 읽을 때마다 데이터를 Response로 뿌려준다. 시간이 지날 때마다 들어오는 Chunk들은 기존 Chunk들에 Concatenate 되어 최종적으로는 하나의 Object가 된다.

Using PDFKit for .pdf Generation

PDF를 불러와서 Stream을 통해 PDF로 보내는 것까지는 괜찮은데, 초기에 불러오는 PDF File의 경우 Hard Code된 것이다. 이렇게 Hard Code말고는 PDF를 생성할 수 없을까? → 가능하다.
pdfkit라는 Third Party Package를 통해서 PDF를 생성할 수 있다.
Import한 pdfkit을 Contructor Method를 통해 PDF를 생성할 수 있다. Constructor Method로 생성한 새로운 상수 역시 Readable Stream이기 때문에 Writable한 File Stream에 Pipe할 수 있다.
즉, Constructor을 통해서 생성한 Readable Stream인 PDF를 Pipe하여 Write가 가능한 Stream인 fs.createWriteStream()으로 보낸다. 이 과정을 통해서 Server에 File을 남기게 된다.
Server에 File을 남기는 것 외에도 Client에게도 보내야 하므로, 해당 PDF를 Pipe하여 Writable Stream인 Response에게도 보내준다.
이렇게 두 가지의 작업이 끝나면 PDF 생성에 대한 Setting이 끝난 것이다. 즉, Document에 어떤 것을 추가하더라도 생성된 File에 데이터들이 Forward되게 된다.
pdfDoc.text()를 통해서 PDF Document에 Text를 1줄 추가할 수 있다.
작업이 끝났다면, pdfDoc.end()를 통해서 pdfDoc에 연결된 File을 생성하는 Writable Stream과 Response를 보내는 Writable Stream을 모두 닫을 수 있다.

Deleting Files

File에 대한 삭제는 오로지 File System Library를 통해서만 지울 수 있다.
File을 삭제해야 하는 경우는 크게 두 가지이다. 항목을 Edit하여 Override하는 경우, 그리고 항목을 삭제하여 아예 File이 존재하지 않아야 하는 경우이다.
File을 삭제할 때는 fs.unlink()를 통해서 수행한다. File의 이름을 삭제하고, 그 이름과 연결된 File 자체를 삭제해주는 기능을 한다. 인자로는 File의 Path와 Error에 대한 Callback Function을 갖는다.