본문 바로가기

javascript/nodejs

서버가 static 파일을 보내는 방법

 서버를 이용하여 파일을 전달하다 보니, 갑자기 static 파일이 어떻게 동작하는지에 대해 궁금해졌다. 사용자는 html 파일만 요청하는데, 그렇다면 css, js 파일 등 초기 페이지 이외의 정보는 대체 어떻게 서버로부터 가져가는 것일까?

https://www.youtube.com/watch?v=SmE4OwHztCc 

 정보를 찾아보니, 내가 생각한 부분에는 아주 큰 오개념이 존재했다. 사실 사용자 측에서는 하나의 html 파일만 요청하지 않는다. 정확히 말하자면, 브라우저는 하나의 html 파일만 요청하지 않는다.

 맨 처음 사용자가 html 파일을 받게 되면, 브라우저는 해당 내용을 분석하고 DOM 이라는 문서 구조로 파싱한다. 해당 작업을 통해 각각의 노드들은 객체로 간주되며, 사용자는 자바스크립트를 통해 이러한 노드들에 접근할 수 있게 된다. 아무튼, html 파일을 분석하여 DOM 을 만드는 과정에서, 브라우저는 css, javascript 등 추가적으로 필요한 정보가 있다는 사실을 알게 된다. 이 시점에 브라우저는 자신과 http 통신을 수행하고 있는 서버에게 해당 파일들을 원한다고 다시 request 를 보낸다. 사실 이러한 정보는 개발자 도구의 네트워크 부분을 보면 잘 드러난다.

 위 이미지를 보면, 초기에 파일을 받은 후에도 추가적인 요청이 있음을 볼 수 있다. 이러한 정보는 해당 파일들에 대한 http 요청 내역을 검사해보면 더 확실하게 드러난다.

 나는 분명히 http://localhost:3000 을 통해 html 파일만 받았지만, 브라우저가 html 파일을 분석한 후, 특정 파일들이 추가적으로 필요하다는 사실을 알게 되고, 이로 인해 html을 받은 서버에 대해 새로운 request를 보낸 것이다. 그렇기에 사실은 html에 필요한 부가적인 파일들은 서버 측에서 추가로 보내는 것이 아니라, 오히려 클라이언트 측에서 요청하는 것이라고 볼 수 있다. 서버는 그저 우직하게 요청된 건을 수행할 뿐이다.

 이때, 위 그림 중 타임 스탬프가 그려진 부분을 다시 한번 보자. 사실 hello 및 world는 원래 자바스크립트 파일이다. 그런데, 해당 파일들에 대한 확장자가 없음에도 불구하고 자바스크립트 파일을 정상적으로 얻게 되었다. 이건 어떻게 된 것일까? 사실 어제 이와 관련된 글을 작성했다.

https://blaxsior-repository.tistory.com/107

 

[error] Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/ht

발생한 에러 생활 코딩 webpack 편을 듣는 도중, html 상에서 모듈을 이용할 때 다음과 같은 에러가 발생했다. 해당 에러가 발생한 환경은 이렇다. html 파일 : Hello, Webpack! express 서버 파일 : import expr..

blaxsior-repository.tistory.com

 이 글에서 express.static 미들웨어에 extensions 관련 설정을 넘기는 것을 보였었는데, 해당 설정의 결과로 입력된 파일이 없다면 'js' 와 같은 확장자를 추가하여 확장자가 누락된 파일이 있던 것은 아닌지 확인하게 된 것이다.

 express.static 에 대한 간단한 구조 살펴보기

express.static 에 대해 vscode 와 같은 툴에서 구현으로 이동하기를 수행하면, 다음과 같은 결과를 보여준다.

 express.static은 사실 serve-static 이라는 모듈이다.  해당 모듈의 깃허브 페이지를 살펴보면, 초반에는 여러가지 설정을 진행하다가, send 라는 함수를 이용하게 되면서 send stream을 만든다고 한다.

https://github.com/expressjs/serve-static/blob/9b5a12a76f4d70530d2d2a8c7742e9158ed3c0a4/index.js#L96

 이때 send 역시 npm 상에 존재하는 별개의 모듈로, 공식 문서에 따르면 Conditional-GET, if-Modified 등 http 에서 통신 효율을 높이기 위해 사용되는 정보에 대응되는 동작을 수행하면서 파일 시스템을 스트리밍하는 모듈이라고 한다. 

 외부로 노출하는 함수인 send는 SendStream 생성자 함수에 대한 객체를 반환한다. send 모듈 자체가 상당히 오래되었기 때문에, 코드가 전반적으로 클래스가 아닌 프로토타입 기반으로 구성되어 있는데, 여기서 SendStream 타입은 util.inherits을 통해 (node에서 여러가지 데이터를 전달하는데 사용되는) Stream 인터페이스를 상속하고 있다.

 코드를 실제로 살펴보면, 정말 많은 기능들이 SendStream.prototype.~ 형태로 정의되어 있다. 이들 중 실제 파일 처리와 관련되어 보이는 메서드를 고르면 Send.prototype.send 가 있다.

https://github.com/pillarjs/send/blob/master/index.js#L602

 해당 함수에서는 Conditional-GET 체크, Headers 및 content-type 설정 등 정말 많은 일들을 수행한 다음에SendStream.prototype.stream을 호출한다.

https://github.com/pillarjs/send/blob/master/index.js#L785

 stream 에서는 처음으로 fs (파일 시스템) 을 이용하여, 파일에 대한 스트림을 연다. 이후, 재정의한 pipe 메서드를 이용하여 결과적으로 sendFile 또는 sendIndex 등의 메서드로 파일을 보내는 것으로 보인다.

 세부적인 구성을 빼고 말하자면, express 에서의 static 폴더 관리는 serve-static에 의해 수행되며, 해당 모듈은 내부적으로 http 헤더 정보를 읽어 해당 통신에 반응하면서 데이터를 관리 및 전달하는 send 모듈을 사용한다고 볼 수 있다. 정말 중요한 부분은 send 모듈에서 각각의 http 헤더의 내용들에 대해 자동으로 대응해주는 덕분에 우리가 편하게 express 기반 서버를 사용할 수 있다는 것을 상기하는게 아닐까 생각된다.

결론

 클라이언트가 특정 html 파일을 요청할 때, 브라우저는 html을 분석하여 DOM을 만들고, 추가적으로 필요한 정보들을 서버에 몇번이고 요청하게 된다. static 파일은 서버가 자동으로 클라이언트 측에 제공하는 것이 아니고, 오히려 클라이언트 측에서 여러번의 request를 통해 서버로부터 제공받는 것이다.

 이때 static 폴더는 serve-static 및 send 모듈 선에서 클라이언트의 http 헤더에 포함된 여러가지 정보에 대한 처리를 맡고 있기 때문에, 개인적으로 Etag나 if-modified 등에 대응할 필요 없이 쉽게 사용할 수 있는 것이다. 이런 편리한 모듈들을 만들어 주신 기여자들에게 감사한 마음을 가져야겠다.