이번 포스팅에선 "nest에서 이미지(혹은 파일)를 업로드 시키는 방법"에 대해 간단히 알아보고자 하며, 구현하는데 있어 부딪히게 되었던 몇가지 상황들을 함께 언급해보고 코드를 개선해보고자 한다.
기록용으로나, 정보 공유용으로나 의미있지 않을까 싶어 간단히 작성해보고자 한다.
Multer
와 Interceptor
를 통한 파일 업로딩타이틀을 위와 같이 정하긴 하였지만, nest에서 파일 업로딩을 처리하는 단계적인 방법에 대한 서술은 하지 않겠다. 필요한 라이브러리 설치 및 사용법 및 대략적 방법은 공식문서에서 알아볼 수 있다.
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
위는 공식문서에서 제시하는 "Basic example" 이다.
위 예시는 정확히 말하면 "단일 파일 업로드 (Single file-upload)"로써 베이직 예시를 토대로 단순히 업로딩 방법을 말하자면 FileInterceptor()
인터셉터를 경로 핸들러에 연결하고 @UploadedFile()
데코레이터를 사용하여 요청에서 파일을 추출할 수 있다.
※ @UploadFile()
은 @nestjs/common
으로 부터 불러온다.
Interceptor
를 사용할까? (Feat. Multer
)파일 업로딩을 하는데 있어 @UseInterceptors()
를 통해 FileInterceptor
를 핸들러에 주입시켜줌으로써 구현해주고 있다.
아주 근본적인 고민으로, 파일을 업로딩하는데 있어서 "왜" 인터셉터를 사용하는 걸까?
"Interceptor(인터셉터)"는 NestJS에서 HTTP 요청/응답의 생명주기 중간에 위치하여 요청/응답을 가로채고 수정하는데 사용된다. 일전에 "Nest에서 응답객체에 어떻게 접근할까?" 라는 포스팅을 통해 인터셉터의 사용에 대해 알아보았다. (해당 포스팅 클릭✔)
그러한 과정에서, 물론 인터셉터의 역할은 다양하겠지만, 주로 "응답을 수정하거나 검증 및 가공하는 작업"에 사용된다는 것을 알게 되었다. 뭔가 이번 이미지 업로딩의 경우도 "url 경로를 가공해주는 작업을 인터셉터에 하니... 그런 맥락에서 사용하는 걸까...?" 라고 생각은 들었지만 조금은 더 자세히 짚고 넘어가자 생각하였다.
✔ Multer
라이브러리를 통해 알아보는 인터셉터의 사용
이미지나 파일을 업로드 할 시 클라이언트는 멀티파트(form-data
)형식으로 요청을 보내게 된다. 이러한 요청을 받게 될 시 서버에선 특별한 처리가 필요하다. 이때, Multer
라이브러리는 Express에서 multipart/form-data
를 처리하는 데 사용된다.
Multer
에 대해 간단히 설명하자면 위에서도 언급하였듯이 파일 업로드를 위해 사용되는 multipart/form-data
를 다루기 위한 node.js의 미들웨어이다. multipart가 아닌 폼에서는 동작하지 않는다. 이렇게 미들웨어 형식으로 제공되는 Multer는 요청이 처리되는 중에 파일을 저장하거나, 파일의 이름을 변경하거나, 파일 크기를 제한 및 유형 검증을 하는 등의 작업을 수행할 수 있다.
즉, NestJS
에서 '@nestjs/platform-express/multer'
를 통해 불러온 FileInterceptor
라는 인터셉터를 통해 Multer
미들웨어를 사용할 수 있는 것이다. 결국, Express
에서 Multer
미들웨어를 사용하여 나타내는 것과 같은 효과를 보게 된다.
그리고 해당 인터셉터는 ExecutionContext
(실행 컨텍스트)를 사용하여 multer()
함수를 호출하고 파일 업로드 요청을 처리하게 된다.
미들웨어 자체로도 구현할 수 있지만, Nest에겐 "인터셉터"란 강력한 기능이 있기 때문에 조금 더 편하게 나타낼 수 있는것이지 않을까 생각하였다.
우리의 코드는 예시코드로써 단순하지만, 이러한 이미지 업로드를 필요로 하는 컨트롤러 및 액션이 많아질수록 인터셉터는 더 강력해질 것이다. 해당 작업을 모든 엔드포인트에서 매번 수행하는 것은 불편하고 굉장히 반복적인 행위일 것이다.
먼저 이미지 업로딩을 위한 전체 코드를 바로 확인해보자. (컨트롤러단에서 전부 구현? 일단은.... 그렇게 하였다.)
// upload.controller.ts
import { Controller, Get, Param, Post, Req, Res, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express/multer';
import { Response, Request } from 'express';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
@Controller()
export class UploadController {
@Post('uploads')
@UseInterceptors(FileInterceptor('image', {
storage: diskStorage({
destination: './uploads',
filename(_, file, callback): void {
const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
return callback(null, `${randomName}${extname(file.originalname)}`)
}
})
}))
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
url: `http://localhost:5000/api/uploads/${file.filename}`
}
}
@Get('uploads/:filename')
async getImage(
@Param('filename') filename: string,
@Res() res: Response
) {
res.sendFile(filename, { root: 'uploads' });
}
}
크게 파일을(이미지) 업로드하는 액션함수 uploadFile
과 파일을 조회하는 액션함수 getImage
가 구현되어있다.
코드에 대한 세세한 설명은 공식 문서 및 다양한 블로그에 충분히 나와있으니 하지 않겠다. (ㅎ) 그래도 간단히 짚고 넘어가 보자.
✔ FileInterceptor()
내부 구현과 사용
@UseInterceptors(FileInterceptor('image', {
storage: diskStorage({
destination: './uploads',
filename(_, file, callback): void {
const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
return callback(null, `${randomName}${extname(file.originalname)}`)
}
})
}))
FileInterceptor()
데코레이터는 두 개의 인자를 가진다.
fieldName
: HTML from data (필드) 에서 name
에 해당하는 문자열 값이다. 우리의 경우 image
가 이에 해당한다.
options
: 이것은 앞서 우리가 언급했던 내용과 연관이 있는 핵심적 부분이다. 해당 옵션은 MulterOptions
타입의 옵셔널 객체이다. 이것이 곧 multer()
생성자가 사용하는 것과 동일한 객체인 것이다.
실제 구현체는 아래와 같다.
import { NestInterceptor, Type } from '@nestjs/common';
import { MulterOptions } from '../interfaces/multer-options.interface';
export declare function FileInterceptor(fieldName: string, localOptions?: MulterOptions): Type<NestInterceptor>;
그럼 우리가 MulterOptions
의 속성 중 하나로 사용하고 있는 storage
를 해석해보자.
storage
는 말 그대로 "저장소"이다. 이미지 (혹은 다른 파일)를 저장하는 방식으로 우리는 diskStorage()
즉, 디스크 저장소를 사용하였다. 하지만 저장하는 방식에는 디스크 뿐 아니라 memoryStorage()
즉, 메모리 저장소가 존재하고 큰 규모에서는 AWS S3
와 같은 방식을 통해 관리하기도 한다. 이러한 방법들의 비교를 통해 각각의 특징을 설명하고 싶지만, 아직 본인도 알아가는 단계이며 글이 루즈해질 것으로 판단해 추후 다뤄보도록 하겠다.
일단, 우리는 이러한 diskStorage 방식을 사용하게 되고 그것에 준수하여 원하는 값을 채워줄 수 있다.
아래는 diskStorage
의 인자로 받게 되는 DiskStorageOptions
인터페이스의 구현체이다.
interface DiskStorageOptions {
/**
* A string or function that determines the destination path for uploaded
* files. If a string is passed and the directory does not exist, Multer
* attempts to create it recursively. If neither a string or a function
* is passed, the destination defaults to `os.tmpdir()`.
*
* @param req The Express `Request` object.
* @param file Object containing information about the processed file.
* @param callback Callback to determine the destination path.
*/
destination?: string | ((
req: Request,
file: Express.Multer.File,
callback: (error: Error | null, destination: string) => void
) => void) | undefined;
/**
* A function that determines the name of the uploaded file. If nothing
* is passed, Multer will generate a 32 character pseudorandom hex string
* with no extension.
*
* @param req The Express `Request` object.
* @param file Object containing information about the processed file.
* @param callback Callback to determine the name of the uploaded file.
*/
filename?(
req: Request,
file: Express.Multer.File,
callback: (error: Error | null, filename: string) => void
): void;
}
우리의 경우는 현재 루트 디렉토리 폴더 아래에 uploads
폴더를 생성해주는 방식을 택하였다. (destination
정의)
또한 filename
을 작성하는데 있어 아래와 같은 조합 방식을 채택하였고,
const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('')
return callback(null, `${randomName}${extname(file.originalname)}`)
이를 콜백의 두 번째 인자로 넣어줄 수 있었다.
randomName
에 해당하는 코드는 예상하였듯이 파일을 저장할 때 파일 이름을 랜덤으로 생성하기 위한 과정이다.
위 코드는 stackoverflow에서 찾을 수 있었다. 일반적으로 multer
에서 중복되지 않는 파일네임을 생성하는데 유용하게 사용할 수 있는 16진수 문자열 생성 코드이다.
물론, 위의 방법보다 uuid
와 같은 조금 더 강력한 라이브러리를 사용하는 것이 바람직할 수 있다.
이렇게 fileName()
은 randomName
과 extname()
을 통해 생성한 파일 확장자(jpg, png, ...)의 조합을 최종 리턴하게 된다.
✔ 액션 (uploadFile
) 및 검증
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
url: `http://localhost:5000/api/uploads/${file.filename}`
}
}
자, 이렇게 우린 이미지 업로드를 구현할 수 있게 되는 것이다. 포스트맨으로 간단히 확인해보면 url이 잘 리턴된 것을 확인할 수 있다.
path
의 문제 (filename vs path
)해당 부분을 앞서 먼저 언급하는게 맞았지만, 흐름이 끊어질 수도 있기에 지금 언급해보고자 한다.
문제가 된 부분은 반환하는 url
경로를 작성하는데 있었다.
return {
url: `http://localhost:5000/api/uploads/${file.filename}`
}
이는 잘 동작한다. 하지만, 난 처음 위와 같이 작성하지 않았다. 처음엔 아래와 같이 file.filename
이 아닌, file.path
로 작성하였다. (file.path
를 먼저 사용한 이유가 딱히 존재하는 것은 아니지만 해당 방법이 맞을 거라 생각했었다.)
return {
url: `http://localhost:5000/api/${file.path}`
}
하지만, 위와 같이 설정하고 POST
요청을 보내면 응답으로 아래와 같이 나오는 것을 확인 할 수 있었다.
{
"url": "http://localhost:5000/api/uploads\\c8dd9290fa43a119e67e2754a004fab2.jpg"
}
uploads
뒤의 파일네임과 연결하는 경로 구분자가 "/
"가 아닌 "\\
"로 받아지는 것이다.
왜 이런 경로 구분자가 출력된 것일까?
✔ file.filename
vs file.path
file.filename
과 file.path
는 비슷하면서도 다르다. 일단 둘 다 multer 모듈이 업로드한 파일 객체의 프로퍼티로, 해당 파일의 경로와 이름을 포함하고 있다.
다만, file.filename
은 파일을 저장할 때 Multer가 임의로 생성한 파일 이름으로, 말 그대로 "파일 이름"이다. 우리가 위에서 randomName
과 확장자를 더해 만든 부분이다.
반면에, file.path
는 업로드된 파일의 실제 경로를 나타내며, 디스크에 저장된 파일의 "경로"이다. 따라서 path
를 사용하면 업로드된 파일의 실제 경로를 반환할 수 있다.
그래서 이 차이랑 "역슬래시"가 무슨 연관인가?🧐
이것은 윈도우 에서 "경로(path
)"를 나타낼 때 사용되는 파일 경로 구분자가 바로 "역슬래시\
"이기 때문이다.
윈도우에서 파일 경로를 지정할 때 \
를 사용하게 될 시 "이스케이프 문자(escape character)"로 인식되어 \u
와 같은 문자열로 치환하게 된다.
우리의 경우엔 파일 경로를 문자열로 구성할 때 \
를 사용하는 경우, 경로 구분자로 인식이되어 \
가 \\
로 자동 변환되는 것이다.
이스케이프 시퀀스에 대해 더 알고 싶다면 마이크로소프트 문서 설명에 도움을 받을 수 있을 것이다. (표를 통해 확인할 수도 있다.)
✔ file.path
를 쓸 경우, Windows에선 어떻게 처리해줄 수 있을까?
일단 \\
문자열이 url에 포함되있으면 해당 경로는 유효하지 않다. 현재는 경로 구분자로 \
와 /
를 모두 인식하므로 해당 문자로 바꿔주는 작업을 수행해야 할 것이다.
만약 file.filename
을 아직 모른다고 가정하고 file.path
를 그대로 사용하면서 원하는 url
을 (즉, 알맞은 경로 구분자를) 얻고자 한다. 이럴땐 어떻게 처리해주면 좋을까?
간단하다. 그냥 replace
를 사용하여 치환해주는 과정만 추가하면 된다. 아주 간단한 정규식이 수행될 것이다.
return {
url: `http://localhost:5000/api/${file.path.replace(/\\/g, '/')}`
}
'g
'는 Javascript 정규표현식에서 전역 검색(Global search)를 의미한다.
전역 검색이란, 대상 문자열에서 패턴과 일치하는 모든 부분을 찾는 것이다. 정규표현식에서 '/
'로 문자열을 구분하며, '/(찾을 문자열)/g'
와 같이 작성할 수 있다.
즉, 위는 '/\\/g'
는 백슬래시 문자열을 모두 찾아, 슬래시 ('\'
)로 치환하는 과정이다.
사실 이럴 필요없이 처음 언급했다시피 file.fileName
을 사용하면 된다. file.fileName
을 사용하게 될 경우, OS별로 경로 구분자가 달라지는 문제를 편하게 대처할 수 있다. 즉, file.fileName
과 같이 , 상대적 경로를 사용하여 url을 나타내는 것이 우리의 경우에서 더 바람직한 방법이라 할 수 있을 것이다.
Controller
에서 인터셉터 구현채 분리하기현재 @UseInterceptor(FileInterceptor)
를 통해 액션함수 uploadFile()
에 FileInterceptor
를 주입하고 있는 형태이다. 동시에 내부 인자 및 그에 해당하는 옵션에 대한 구현을 전부 컨트롤러 단에서 작성해주었다.
물론, 우리의 경우와 같은 짧은 코드에선 다음과 같이 작성해도 상관없지만 컨트롤러단에 직접적으로 로직이 들어가지 않도록 하는 것이 좋다. MulterOptions
를 정의하는데 있어 작성할 것이 많아질수록 더 느껴질 것이다.
✔ Custom Interceptor
를 사용하여 분리시켜보자.
// upload-interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from "@nestjs/common";
import * as multer from 'multer';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
import { Observable } from "rxjs";
@Injectable()
export class UploadInterceptor implements NestInterceptor {
private upload: RequestHandler;
constructor() {
this.upload = multer({
storage: diskStorage({
destination: './uploads',
filename: (_, file, callback) => {
const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('');
return callback(null, `${randomName}${extname(file.originalname)}`);
},
}),
}).single('image')
}
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> | Promise<Observable<any>> {
const req = context.switchToHttp().getRequest();
return new Observable(observer => {
this.upload(req, null, async (err: any) => {
if (err) {
observer.error(err);
} else {
next.handle().subscribe(observer);
}
});
});
}
}
NestJS에서 제공하는 NestInterceptor
인터페이스를 구현하게 되는 커스텀 인터셉터인 UploadInterceptor
를 위와 같이 작성할 수 있다.
기존에 비해 눈에 띄는 차이점이 바로 보일 것이다.
바로 constructor
내부에서 multer
함수를 직접 정의한다는 것이다.
즉, 이에 따라 multer
모듈을 불러올 필요가 생긴다. 앞서 컨트롤러단에서 FileInterceptor
를 사용하였을 땐, 해당 인터셉터를 사용함으로써 multer
함수를 따로 만들어줄 필요없이 제공받을 수 있었다. 반면에 커스텀 인터셉터의 경우 직접 불러옴으로써 구현체를 만들 수 있게 된다.
FileInterceptor
의 첫 번째 인자로 받아온 fieldName
과 같은 경우, 이번 케이스에선(multer
함수를 직접 정의한 경우) 함수의 마지막에 체이닝 형식으로 .single('필드 이름)
와 같이 받아오게 된다.
다음은 intercept()
메서드의 구현이다. 해당 메서드는 알다시피 인터셉터를 구성하는데 있어 필수적인 조건이다. 인자로써 Execution Context
(실행 컨텍스트)와 CallHandler
를 받게 되며, 이를 통해 요청 응답을 가로채고 요청을 처리하게 되는 액션함수인 컨트롤러 메서드가 수행할 수 있도록 한다. (우리의 코드에선 uploadFile()
메서드가 될 것이다.)
const req = context.switchToHttp().getRequest();
위와 같이 요청 객체를 먼저 생성한다. 해당 요청 객체는 미들 웨어 실행시 사용될 것이다. ( 요청 객체를 만들기 위한 switchToHttp()
, getRequest()
와 같은 메서드에 대한 설명은 생략하겠습니다. 조금 더 자세히 알고싶다면 저의 앞선 블로그를 참조해주시기 바랍니다. ⬇⬇)
Execution context __docs 번역작업 ✔
그 뒤, 아래와 같이 Observable
을 생성해 비동기적으로 미들웨어를 실행하게 된다.
return new Observable(observer => {
this.upload(req, null, async (err: any) => {
if (err) {
observer.error(err);
} else {
next.handle().subscribe(observer);
}
});
});
Observable
은 인터셉터에서 필수적으로 써야하는 조건은 아니다. 그렇지만 파일 업로드와 같은 비동기 작업을 처리할 경우 Observable
클래스는 유용하며 강력하다. 먼저 Observable
을 생성하고 해당 Observable
을 반환하여 파일 업로드 작업이 완료될 때까지 대기하도록 한다. 이렇게 함으로써, 업로드가 완료된 후에 다음 핸들러로 넘어가도록 보장하는 것이다.
코드에 대해 설명하자면, 일단 this.upload()
가 앞서 multer()
를 통해 구현한 Multer 미들웨어를 실행하는 함수이다.
첫 번째 인자인 req
는 현재 요청 객체이고, 두 번째 인자인 null
은 options
에 관한 부분이다. 현재 null
로 설정한 이유는 multer
의 options
를 생략하고, diskStorage
설정만 사용하는 것을 의도한다. null
을 넘겨줘도 무방한 것이다.
이것에 대한 설명은 multer
공식문서에 나와있지만 사실 스스로도 정확히 이해가 가지 않았다. 옵션이라는 것이 어디까지에 해당이 되는 것인지도 잘 모르겠고 뭔가 불분명하지 않나 생각하였다.
const req = context.switchToHttp().getRequest();
const res = context.switchToHttp().getResponse();
return new Observable(observer => {
this.upload(req, res, async (err: any) => {
if (err) {
observer.error(err);
} else {
next.handle().subscribe(observer);
}
});
});
그래서 다음과 같이 res
객체를 생성해 두 번째 인자로 받아주는 것을 택하였다.
마지막으로, if ...else
문을 확인해보자.
업로드가 성공한 경우, next.handle()
메서드를 실행하고 해당 메서드가 반환한 Observable
객체를 subscribe()
메서드를 호출하여 "구독" 한다. 이를 통해 next.handle()
메서드가 반환한 값을 observer
객체를 통해 다음 미들웨어 혹은 핸들러로 전달할 수 있다.
✔ 적용하기 (Controller
주입) 및 생각정리
@Post('uploads')
@UseInterceptors(UploadInterceptor)
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
url: `http://localhost:5000/api/uploads/${file.filename}`
}
}
다음과 같이 @UseInterceptors()
데코레이터의 인자로 우리가 작성해준 UploadInterceptor
를 받을 수 있다. 이렇게 우린 커스텀 인터셉터를 uploadFile()
메서드에 주입시켜준 것이다.
기존에 비해 컨트롤러 (혹은 라우트 핸들러 함수) 부분이 확연히 깔끔해진 것을 알 수 있다. 물론, 커스텀 인터셉터를 작성하는 과정에 있어 복잡한 개념과 일일히 인터셉터 함수를 작성해주어야하는 번거러움이 존재하지만 이것은 절대 오버헤드라 할 순 없다.
앞서 언급하였듯이, 지금은 이미지 업로드 과정에서 단순 diskStorage
설정만 해주었지만 만약 더 많은 옵션을 (fileFilter, limits, ...
) 설정하게 될 시 컨트롤러의 가독성이 떨어질 것이다. 또한, 꼭 FileInterceptor
가 아니더라도 직접 인터셉터를 생성함으로써 라이브러리-의존성(Library Dependency
)를 조금씩 벗어나는 연습또한 유의미하다고 본다.
Middleware & Interceptor
)nestJS에서 파일 업로딩을 어떻게 처리하는가에 대해 알아보던 중 생각보다 해당 구현법에 포커싱이 아닌 "nestJS에서 인터셉터의 의미" 에 대해 더 깊이 생각해본 것 같았다.
포스팅 도중에 "윈도우 os에서 파일 경로 구분자 문제로 인한 이슈" 에 대해서도 잠깐 소개하였지만, 사실 이번 포스팅의 핵심은 인터셉터의 존재성과 nest가 바라보는 미들웨어와 인터셉터에 초점을 맞추었다.
multer
는 익스프레스에서 사용되는 파일 업로드 라이브러리로 "미들웨어" 함수를 제공한다. 그럼 인터셉터를 사용해 multer
를 불러오게 되면 미들웨어가 아닌가? 그건 또 아니다. 미들웨어와 인터셉터는 둘 다 요청 응답 주기에 접근함으로써 특정 임무를 수행하게 된다. 물론 상황에 따라 어떤 것을 쓰는 것이 더 유효하고 활용적인지는 명확하지만 항상 와닿지 않았다.
아래는 단순 검증차 작성해본 이미지 업로드 요청/응답 접근에 관한 미들웨어이다.
// image-upload.middleware.ts
import { NextFunction, Request, Response } from 'express';
import { diskStorage } from 'multer';
import * as multer from 'multer'
import { extname } from 'path';
export const imageUploadMiddleware = () => {
return multer({
storage: diskStorage({
destination: './uploads',
filename: (req: Request, file: Express.Multer.File, callback: (error: Error | null, filename: string) => void) => {
const randomName = Array(32)
.fill(null)
.map(() => (Math.round(Math.random() * 16)).toString(16))
.join('');
return callback(null, `${randomName}${extname(file.originalname)}`);
},
}),
}).single('image');
};
export const getImageUrlMiddleware = () => {
return (req: Request, res: Response, next: NextFunction) => {
const filename = (req as any).file.filename; // 이미지 파일명
const imageUrl = `http://localhost:5000/api/uploads/${filename}`; // 이미지 url
// 이미지 url을 res.locals 객체에 저장
res.locals.imageUrl = imageUrl;
next();
}
};
아래의 컨트롤러 라우트 핸들러를 통해 미들웨어 함수를 받아올 수 있다.
// upload.controller.ts
@Post('upload-image-with-middleware')
async uploadImageWithMiddleware(@Req() req: Request, @Res() res: Response) {
imageUploadMiddleware() (req, res, (err: any) => {
if (err) {
return res.status(400).json({message: 'image upload failed'})
}
return res.status(201).json({ message: 'image upload succeed'})
})
}
사실, 어느정도까지 미들웨어가 인터셉터의 역할 또한 할 수 있을 것이다. 이건 어디까지나 견해에 불과할 수 있지만, 미들웨어를 여전히 네스트에서도 유용하게 활용하지만 네스트에선 인터셉터, 가드, 파이프 등과 같은 특정 상황에서 더욱 유용한 기능들이 도입되었다.
흔히, 익스프레스를 미들웨어기반의 프레임워크라고 하듯이, 미들웨어로만 처리하던 기존과 달리 네스트는 다양한 기능들을 통해 효과적으로 접근할 수 있게 된것이 아닐까 싶다. 더군다나 네스트에서 미들웨어는 "Execution Context(실행 컨텍스트)"에 접근하지 못한다. 즉, 실행 컨텍스트에 접근할 수 있는 다른 녀석들에 비해 조금은 더 단순한 기능을 처리하는데 한정적이기도 하다.
어쩌다보니, 단순 파일 업로드를 공부하는 중에 네스트의 라이프 사이클까지 건드리게 되었다. 하지만, 충분히 유의미한 공부였고 나같은 네스트 입문자 및 초보자들에겐 한번씩 생각해보면 좋은 개념이라고 본다.
읽어주셔서 감사합니다..!😎