Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

pushState를 사용하는 SPA를 S3와 CloudFront로 서비스하기

AWS의 스토리지 서비스인 S3에는 웹 호스팅 기능을 제공하고 있어서 정적 웹사이트일 때 HTML, CSS, JavaScript 파일을 올려놓고 웹사이트를 운영할 수 있다. 간단한 정적 웹사이트는 서버 운영 걱정 없이 사이트를 제공할 수 있고 S3가 죽는 경우는 흔치 않으므로 웹사이트를 안정적으로 제공할 수 있다. 정적 파일은 CDN을 제공하는 게 좋으므로 보통 S3 앞에 CloudFront를 연결해서 제공하는 것이 일반적이다.

S3 웹사이트 호스팅

먼저 Terraform으로 S3 웹사이트 호스팅을 설정해보자.

resource "aws_s3_bucket" "website" {
  bucket = "demo.example.com"
  acl    = "private"
  policy = "${data.aws_iam_policy_document.website.json}"

  website {
    index_document = "index.html"
  }
}

data "aws_iam_policy_document" "website" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["arn:aws:s3:::demo.example.com/*"]

    principals {
      type        = "AWS"
      identifiers = ["${aws_cloudfront_origin_access_identity.website.iam_arn}"]
    }
  }
}

정적 파일을 저장할 S3 버킷을 생성하고 CloudFront에서 이 S3 버킷 파일에 접근 할 수 있도록 권한을 부여했다.

resource "aws_cloudfront_origin_access_identity" "website" {
  comment = "website demo Cloudfront"
}

resource "aws_cloudfront_distribution" "website" {
  origin {
    domain_name = "${aws_s3_bucket.website.bucket_domain_name}"
    origin_path = ""
    origin_id   = "${aws_s3_bucket.website.id}"

    s3_origin_config {
      origin_access_identity = "${aws_cloudfront_origin_access_identity.website.cloudfront_access_identity_path}"
    }
  }

  aliases             = ["demo.example.com"]
  comment             = "demo.example.com"
  enabled             = true
  is_ipv6_enabled     = false
  default_root_object = "index.html"
  http_version        = "http2"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS"]
    cached_methods   = ["GET", "HEAD", "OPTIONS"]
    target_origin_id = "${aws_s3_bucket.website.id}"

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    max_ttl                = 360
    default_ttl            = 60
  }

  price_class = "PriceClass_All"

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

이번엔 CloudFront 설정이다. 정적 파일을 CDN으로 제공하기 위해 CloudFront를 생성하고 그 대상을 앞에서 만든 S3 버킷으로 지정했다. 물론 실제로 서비스를 한다면 여기서 Route53과 ACM을 이용해서 도메인을 연결하고 인증서를 붙이겠지만 여기서는 크게 중요하지 않으므로 생략한다.

이렇게 설정하고 나면 CloudFront의 도메인이나 Route53으로 설정한 도메인을 통해서 웹사이트에 접속할 수 있다.

Push State를 사용하는 SPA

SPA는 Single Page Application의 약자로 React 등이 프론트엔드의 기술로 자리잡으면서 요즘은 많이 사용하고 있다. 웹사이트를 SPA로 만드는 경우 백엔드 API를 사용하지만, 페이지 자체는 정적 웹사이트이기 때문에 S3의 웹 호스팅 기능을 이용해서 서비스하기 좋다. SPA롤 웹사이트를 만들면 URL에 따라 라우팅을 해야 하는데 anchor(#)를 이용해서 라우팅하거나 Push State를 이용해서 URL을 조작할 수 있다. SPA에서 URL을 변경할 때 중간에 #이 있으면 전자이고 일단 웹사이트와 같게 /board/1 같은 식의 URL이라면 Push State를 사용한 것이다.

S3를 이용한 웹사이트 호스팅에서 index_document = "index.html"로 설정되어 있으므로 demo.example.com에 접속하면 자동으로 index.html을 보여주지만, 그 외의 파일을 경로에 맞게 보여준다. 즉 demo.example.com/app.js를 요청하면 S3 버킷에 app.js가 있어야 한다.

SPA는 최초 요청에서 index.html을 받아서 앱을 초기화한 뒤 라우팅을 하므로 페이지를 이동하면 각 페이지의 URL이 /newest/1이나 /show/5 같은 식이 될 수 있다. 처음 로딩하고 웹을 사용할 때는 잘 동작하지만, URL을 복사해서 demo.example.com/show/5로 접속한다면 S3 버킷에 show/5라는 파일이 없으므로 403 오류를 받게 된다. 원래대로라면 404여야 하지만 S3를 쓰고 있어서 403이 반환된다.

이 문제를 해결하려면 / 밑으로 어떤 URL로 요청이 오던 간에 /index.html을 반환하도록 처리해야 한다. nginx 등의 웹서버가 있다면 라우팅 설정을 하면 되지만 S3 웹사이트 호스팅에서 이 문제를 해결하려면 CloudFront의 커스텀 오류 페이지 기능을 이용해야 한다.

aws_cloudfront_distribution 설정에 아래 custom_error_response 설정을 추가한다.

custom_error_response {
  error_code         = 404
  response_code      = 200
  response_page_path = "/index.html"
}

이는 404 오류일 때 /index.html 파일을 내려주고 응답 코드는 200을 반환하라는 의미이다. 이렇게 하면 jscss 같은 파일을 계속 제공하지만, Push State 라우팅으로 인한 페이지가 없는 경우에는 /index.html가 반환되게 되고 SPA는 이 URL을 분석해서 URL에 맞는 페이지를 보여주게 될 것이다.

2018/08/11 20:22 2018/08/11 20:22