[AWS] Nextjs cloudfront S3 배포 시 html 확장자 제거 (feat. lambda 함수 이용)

2022년 04월 08일 by Xion

    [AWS] Nextjs cloudfront S3 배포 시 html 확장자 제거 (feat. lambda 함수 이용) 목차

이번 글은

Nextjs로 AWS에 베포를 진행하는 과정 중 발생하였다.

 

처음에는 모든 페이지들이 정상적으로 작동하는 듯 싶었으나, 역시 그럴일이 없었다.

 

Nextjs는 동적 라우팅 즉, Dynamic Loute를 지원한다.

ex)  www.~~~.com/products/1  

 

위의 링크처럼 동적 라우팅이 있는 폴더 구조가 있다고 생각해보자.

결국 folder struct는 products 폴더 안에 [id].tsx와 같은 파일이 존재할것이다.

 

여기까지는 아주 좋다.

 

이제 문제는 지금부터다.

AWS의 CloudFront를 이용하여 배포를 진행하였을 때 해당 동적 페이지로 들어가면 아무 페이지가 뜨지 않는다.

 

해결 방법은 .html 확장자를 명시적으로 넣어주는것이다.

 

하지만 매 페이지 요청 시 

www.~~~.com/products/1.html   

 

위와 같이 매번 .html 확장자를 적어줘야한다.

아니면 없는 페이지라고 접속을 못 한다.

 

이유는 왜 그럴까?

 

React와 같은 프레임워크는 SPA이다. 즉 single page application이다.

react가 build가 되면 html 파일을 만든다.

 

next js 같은 경우 동적 페이지가 존재하면 해당 폴더들을 html 파일로 변환해준다.

 

따라서 빌드 된 파일들을 S3에 업로드 하게 되면, html 확장자를 가진 파일들이 업로드가 된다.

 

 

build된 파일들이.html 확장자를 갖고 있다.

 

이렇게 되면 cloudfront url로 해당 파일의 경로를 접속할 때 

 

ex) www.~~~.com /selfconsultation.html 

 

위와 같은 .html 확장자를 넣은 주소로 들어가야지 정상적인 페이지가 로드된다.

 

 

그렇다면, .html을 제거하고 접속할 수 없을까?

 

열심히 구글링을 해봤을 때

 

방법은 몇 개 있었는데 그 중 제일 효과적인 방법을 찾았다

 

내가 적용한 건 두 가지었는데

 

1. 첫 번째 방법

파일명을 직접 변경한다.

S3 bucket에 들어있는 file name의 .html 확장자를 제거한다.

 

하지만?

이렇게 직접 바꿨을 때, 파일들이 절대 변하지 않다는 전제하에 바꿔야한다.

 

무슨소리냐?

다시 한 번 생각해보면 build할 때 .html 파일들을 만들어준다고 했습니다.

그렇다면 직접 파일명을 바꿔주면 덮어씌워지기는 커녕

 

selfconsultation.html 파일 1개와

selfconsultation 파일 1개

 

총 2개의 파일이 생성된다.

그렇다면, build할 때 마다 .html파일이 생성되고 기존에 직접 바꿨던 파일은 그냥 가만히 구 버전으로 남겨지게 된다.

(상당히 번거롭겠죠?)

 

 

2. 두 번째 방법

두 번째 방법은 AWS lambda function을 활용하는것이다.

이 기능을 설정할 때 상당히 삽질을 많이 했다.

 

AWS lambda function

- 인터셉터 같은 역할을 해주어 suffix에 나오는 .html 확장자를 자동으로 제거해주어 사이트를 안내해주는 기능을  추가해준다.

단순하게 AWS에서 제공해주는 함수라고 생각하자.

 

 

 

1.함수를 하나 생성하여 한다

 

(람다펑션 설정 시 AWS Lambda - 함수 - 구성 - 실행 역할 여기를 IAM에서 커스텀으로 만들어준 역할로 바꿔야한다.)

 

 

2.함수 코드 작성해주자 (html 확장자 제거해주는 코드)

 

const config = {
    suffix: '.html',
    appendToDirs: 'index.html',
    removeTrailingSlash: false,
};

const regexSuffixless = /\/[^/.]+$/; // e.g. "/some/page" but not "/", "/some/" or "/some.jpg"
const regexTrailingSlash = /.+\/$/; // e.g. "/some/" or "/some/page/" but not root "/"
// const dynamicRouteRegex = /\/subpath\/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/; // e.g /urs/some-uuid; // e.g. '/subpath/uuid'
const dynamicRouteRegex = /^(.+)\/\d+\/?$/; // e.g. /post/123

exports.handler = function handler(event, context, callback) {
    const { request } = event.Records[0].cf;
    const { uri } = request;
    const { suffix, appendToDirs, removeTrailingSlash } = config;


    if(uri.match(dynamicRouteRegex)) {
        request.uri = uri.replace(dynamicRouteRegex, '$1/[id].html')
        callback(null, request);
        return;
    }
    
    // Append ".html" to origin request
    if (suffix && uri.match(regexSuffixless)) {
        request.uri = uri + suffix;
        callback(null, request);
        return;
    }
    
    // Append "index.html" to origin request
    if (appendToDirs && uri.match(regexTrailingSlash)) {
        request.uri = uri + appendToDirs;
        callback(null, request);
        return;
    }

    // Redirect (301) non-root requests ending in "/" to URI without trailing slash
    if (removeTrailingSlash && uri.match(/.+\/$/)) {
        const response = {
            // body: '',
            // bodyEncoding: 'text',
            headers: {
                'location': [{
                    key: 'Location',
                    value: uri.slice(0, -1)
                 }]
            },
            status: '301',
            statusDescription: 'Moved Permanently'
        };
        callback(null, response);
        return;
    }

    // If nothing matches, return request unchanged
    callback(null, request);
};

 

 

3. 트리거를 추가로 만들어준다

 

현재 저는 로드 발란서와 routre53 통하여 domain을 연결해준다.

따라서 로드 발란서에세 트리거를 부착하여 도메인에 접속 전 html을 먼저 제거 후 안내해 주도록 하였다.

 

 

먼저, 트리거와 cloudfront(저는 로드밸런서이므로)를 연결하여 준다.

트리거 구성에서 cloudfront(저는 로드밸런서)를 추가시켜준 후

 

 

 

완성 예시