문제의 발단은 이렇습니다.

1) 웹브라우저가 다음과 같이 요청.
GET http://www.any-domain.com/any-route/

2) http://www.any-domain.com/ 웹서버가 자동으로 /any-route/ 뒤에 index.html 을 추가하고 응답.
RES /any-route/index.html

3) 웹브라우저가 index.html 안에 포함된 <script src="scripts/any-script.js"> 를 보고 
any-script.js 요청.
GET http://www.any-domain.com/any-route/scripts/any-script.js

4) http://www.any-domain.com/ 웹서버가 /any-route/scripts/any-script.js 를 응답.
RES /any-route/scripts/any-script.js

5) 웹브라우저가 다음과 같이 요청.
GET http://www.any-domain.com/any-route

6) http://www.any-domain.com/ 웹서버가 자동으로 /any-route 뒤에 /index.html 을 추가하고 응답.
RES /any-route/index.html

7) 웹브라우저가 index.html 안에 포함된 <script src="scripts/
any-script.js"> 를 보고 요청.
GET http://www.any-domain.com/script/
any-script.js

8)
 http://www.any-domain.com/ 웹서버가 /scripts/any-script.js 를 찾을 수 없으므로 404 에러 발생.
RES /scripts/any-script.js - Error 404

즉, 문제는 /any-route 뒤에 / 가 붙지 않았을 때 index.html 은 응답하는데 scripts/any-script.js 는 경로가 /any-route 아래가 아닌 / 로 웹브라우저가 인식하는 것입니다.
생각해 보면 당연한 것일 수도 있는데, 웹브라우저는 any-route 가 실제로 파일인지 directory 인지 알지 못합니다. 설사 알고 요청한 것 일지라도 웹서버의 실제 내용은 아닐 수 있습니다. 그래서 웹브라우저가 /any-route 를 요구했을 때, 웹서버가 index.html 을 보내주어도 웹브라우저는 index.html 이 아니라 /any-route 라는 / 밑의 any-route 라는 파일을 받은 줄 압니다. any-route 의 경로가 / 에 있으므로 script/any-script.js 도 / 에 위치한 줄 알고 웹브라우저는 /script/any-script.js 을 요구하게 됩니다.

이 문제를 해결하는 가장 쉬운 방법은 redirect 로 다시 / 가 뒤에 붙은 url 을 돌려 주는 것입니다.
즉, /any-route 을 요구 받으면 웹서버가 우선 any-route 가 파일인지 디렉토리인지 판단한 후 디텍토리이면 /any-route/ 로 redirect 시키면 됩니다. 그러면 웹브라우저는 /any-route/ 로 다시 요청하면 되죠. 사실, connect.js 의 static middleware 는 기본 설정이 이런 동작을 하도록 되어 있습니다.

var connect = require('connect');
var server = connect.createServer()
  .use(connect.static(__dirname))
  .listen(80, 'www.any-domain.com');

> node server.js

node로 실행하면 server.js 가 실행된 위치에서 ./any-route/index.html 과 ./any-route/scripts/any-script.js 가 잘 읽히는 것을 알 수 있습니다. server.js 가 /any-route 를 요구 받았을 때, connect.static 에서 /any-route/ 로 redirect 시키기 때문에 자연스럽게 위의 문제가 해결됩니다.

그런데!!!

사실 위의 코드는 좀 문제가 있습니다. 불필요하게 외부에 server.js 와 같은 위치의 파일들이 노출되어 버리죠. http://www.any-domain.com/server.js 도 외부에서 볼 수 있습니다. 따라서 다음과 같이 고치는 것이 바람직 합니다.

var connect = require('connect');
var server = connect.createServer()
  .use('/any-route', connect.static(__dirname + '/public'))
  .listen(80, 'www.any-domain.com');

> node server.js

server.js 와 같은 위치에 있는 ./any-route/index.html 과 ./any-route/scripts/any-script.js 는 ./public/index.html 과 ./public/scripts/any-script.js 로 이동시킵니다.
server.use()의 첫번째 파라메터는 요청된 URL의 path 가 일치할 때 두번째 파라메터인 핸들을 수행하도록 합니다. 위와 같이 설정하면 /any-route 로 시작하는 URL의 request는 /any-route 가 제거된 URL이 __dirname + '/public' 에 위치한 파일들과 매치됩니다. 복작하게 router 와 app.get() 으로 패턴매칭하지 않아도 손쉽게 경로를 바꿀 수 있습니다.

그러나!!!

위의 수정된 코드는 이전의 경로 문제는 깔끔하게 해결했지만 처음에 언급했던 / 의 redirect 문제를 발생시킵니다. connect.static 에서 잘 처리되었던 redirect 이 왜 이 경우에는 동작하지 않는지 당황스럽죠.

문제는 connect.static 은 root 에 대해서는 redirect 를 하지 않기 때문에 발생합니다.
use()의 첫번째 파라메터로 넘겨준 route path 가 매치되면 connect.static 은 그 이후의 URL 경로부터 검색을 시작하는데 URL 경로가 더 이상 없으면 (정확히 /any-route 이면) 자동으로 / 를 URL path 로 지정합니다. 그리고 path 가 / 로 끝나므로 index.html 을 추가하고 index.html 을 응답으로 보내 버립니다.
만약 /any-route/scripts 와 같은 URL 경로가 왔다면, connect.static 은 /scripts 를 URL path 로 인식하고 /scripts 가 디렉토리인 것을 확인 한 후 /scripts/ 로 redirect 했을 것입니다. 오직 route path 가 정확히 일치할 때만 redirect 하지 않습니다.
/ 가 route path 였으면 redirect 를 하지 않아도 별 문제는 없었겠죠. 하지만, 일단 route path 가 / 이외의 path 로 설정되었을 때 index.html 안의 모든 상대 경로는 index.html 의 위치가 아닌 index.html 의 상위 디렉토리로 어긋나 버립니다.

이 모든 것을 무시하고 동작시키는 방법은 있죠.
index.html 안의 <script src="scripts/any-script.js"> 를 <script src="/any-route/scripts/any-script.js"> 처럼 절대 경로로 지정하면 됩니다. 동작은 하긴 하는데, 개발 중에 route path 가 변경되거나 웹서버를 다른 경로에 설치하면 모든 html 의 경로명을 수정해야 합니다. 단순히 노가다를 하는 것이 나쁘다는 것이 아니라 설치와 유지 보수가 어려워지므로 절대로 피해야 할 방법입니다.

connect.static 의 버그이긴 한데 우선 다른 방법으로 수정해 보지요.

var connect = require('connect');
var server = connect.createServer()
  .use(function(req, res, next) {
    if (/^\/any-route$/.test(req.url)) {
      res.statusCode = 301;
      res.setHeader('Location', req.url + '/');
      res.end('Redirecting to ' + req.url + '/');
      return;
    }
    next();
  }) 
  .use('/any-route', connect.static(__dirname + '/public'))
  .listen(80, 'www.any-domain.com');

> node server.js

req.url 을 normalize 하지 않고 단순히 test 하는게 좀 불안하긴 하지만 우선은 동작합니다.
connect.static 이 정식으로 패치되기 전에는 이런식으로 사용해도 무난할 듯 합니다.
 
Posted by luuvish