이번에 회사에서 이미지를 적절히 최적화해야 할 필요가 있었다.
이를 요즘 프론트엔드에서 사용하는 NextJS라는 프레임워크와 묶어서 AWS의 설명을 곁들여 풀어보려고 한다.
웹 개발을 하다보면 최적화라는 말을 자연스럽게 전해듣는다. 가령 소스코드에서 최적화할 수도 있고 배포과정에서 빌드시간을 최적화한다던가 개발자와 최적화는 땔 수 없는 관계이다.
왜 개발자는 최적화에 땔 수 없는 사이이고 그만큼 중요한걸까?
이는 유저의 경험과 직결되는 일이기 때문이다. 필자는 온라인 게임을 좋아해서 안 해본 온라인 게임이 없을정도인데 배틀그라운드가 초기에 최적화가 잘 되지 않아 프레임이 잘 나오지 않는다는 말이 많았다.
현존하는 모든 PC 게임을 통틀어도 독보적일 정도로 최적화가 엉망이다. 그다지 좋아보이지 않는 그래픽(2011년작 배틀필드 3보다도 못 한 수준이다.)에도 불구하고 요구 사양이 매우 높은 편인데, 기본적으로 CPU를 비롯한 사양을 엄청나게 많이 타는데다 CPU에 비례해도 높은 프레임을 뽑지 못한다.
-나무위키-
당시에는 배틀그라운드가 당연히 고사양게임이라서 좋은 컴퓨터가 필요한 줄 알아서 꽤 고가의 데스크탑을 맞췄는데 이제와서 보니 그럴 필요까지는 없는 서비스였던것 같다. 사람들이 많이 하는 왠만한 컴퓨터에서 다 돌아가는 리그오브레전드라는 게임과 비교하면 배틀그라운드는 꽤나 무거운 게임 서비스이다.
당연히 최적화가 잘 되지 않을수록 유저의 경험이 좋지 않을것이고 이는 서비스의 매출과도 직결된다. 그러므로 개발자가 어플리케이션을 최적화한다는것은 어쩌면 선택이 아니라 필수일지도 모른다.
이번에 회사에서 이미지를 다뤄야하는 개발이 있었고 이 때 다뤄본 경험을 비추어 말하려고한다.
그 전에 우리가 자주 사용하는 인스타그램을 한번 들여다보자. 인스타에서는 스토리나 피드 사진을 제외하고는 프로필사진이 상당히 많이 쓰이는데
위에서부터 마이페이지의 프로필 사진, 메인화면에서의 내 프로필 컴포넌트, 모바일 화면에서의 내 프로필 컴포넌트이다.
다음 사진들은 1배수이기 떄문에 실제 웹에 렌더링 된 사이즈라고 봐도 무방하다. 이 이미지들의 요청 URL을 한번 살펴보자.
https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-19/70497628_571332086938553_1505510410910957568_n.jpg?stp=dst-jpg_s150x150&_nc_ht=scontent-ssn1-1.cdninstagram.com&_nc_cat=105&_nc_ohc=z2DoAd7qli4AX_dY6ay&edm=ACWDqb8BAAAA&ccb=7-5&oh=00_AfBt-dbFRC5WxGATN6n6MsoVYecqXXdXqf_ZRjTSpYEVXg&oe=658AF46B&_nc_sid=ee9879
https://scontent.cdninstagram.com/v/t51.2885-19/70497628_571332086938553_1505510410910957568_n.jpg?stp=dst-jpg_s150x150&_nc_ht=scontent.cdninstagram.com&_nc_cat=105&_nc_ohc=z2DoAd7qli4AX-JjaHL&edm=APs17CUBAAAA&ccb=7-5&oh=00_AfB7EDrcjIfi7dPbAz9KZtyTPuCJXVOKmGYLTwB8adW9lA&oe=658AF46B&_nc_sid=10d13b
https://scontent-ssn1-1.cdninstagram.com/v/t51.2885-19/70497628_571332086938553_1505510410910957568_n.jpg?stp=dst-jpg_s150x150&_nc_ht=scontent-ssn1-1.cdninstagram.com&_nc_cat=105&_nc_ohc=z2DoAd7qli4AX_dY6ay&edm=AAAAAAABAAAA&ccb=7-5&oh=00_AfAIQbo1LYlQvtXsXrTKV6Rn0tH-m_X58w0zGXlvWNIDuQ&oe=658AF46B&_nc_sid=2c5659
똑같은 사진임에도 요청 URL이 다르고 다른 이미지를 내려주는것을 알 수 있다.
그럼 누군가 말한다. 왜 굳이 똑같은 사진을 다른 이미지를 내려주는걸까? 똑같 이미지를 사용하면 캐싱하여 굳이 요청을 하지 않아도 되고 사용성이 오히려 증가한다고.
반은 맞고 반은 틀린 말인데 모든 이미지가 똑같은 페이지(컴포넌트)내에 있다면 이는 어느정도 일리 있는 말이다. 다만 만약 각 이미지가 다른 페이지에 있고 20x20사이즈의 이미지를 표현하기위해 200x200의 이미지를 가지고와야 한다면 불 필요한 이미지 크기를 유저가 감당해야 하고 이는 유저의 경험과도 직결될 것이다.
이를 위해 다양한 방법이 있다.
우선 필자의 회사에서 사용한 방법인데 AWS S3에 Large,Medium,Small 사이즈로 변환한 사진을 업로드 한다. 그리고 실제 이미지가 필요한 서비스에서 필요한 이미지에 aws엔드포인트/images/사진-large.webp 와같은 URI를 가지고 사용한다.
위 사진은 실제 사내 서비스에서 요청한 이미지 결과물이다.
맨 앞에 cloudFront라는 AWS CDN(콘텐츠 전송 네트워크)서비스가 붙었고 이 서비스는 가장 효율적인 네트워크 경로를 통해 콘텐츠를 빠르게 전달하는 서비스이다.
우리가 배포한 파일들에 대해서 직접 접근하기 이전에 중간에 최적의 네트워크를 제공함과 동시에 이를 캐싱하여 비용 절감하는 이유가 있다.
S3 접근 비용 > CDN 서비스 캐싱 접근비용
왜 이 비용이 더 적게드냐면 AWS기준으로 S3는 업로드한 파일의 용량의 비용과 요청의 비용을 책정한다. 그리고 이는 아마존에서 제공하는 방식을 통하여 하드웨어(데이터 저장소)어에 적재된다.
하지만 AWS CloudFront는 자주 사용되는 파일들에 한하여 아마존의 거대한 RAM에서 받아들인 요청에 대한 결과물을 제공하고 이는 일반적으로 S3비용보다 적게 든다.
즉 캐시된 파일에 대한 적재는 아마존 내부의 RAM에 적재하고 이를 서빙한다. 필자가 알기로 RAM에 적재하는 캐시파일이 그렇게 크지 않을 뿐더러 이미 거대하게 구축돼있기 때문에 적재비용은 거의 들지 않거나 안 드는걸로 알고있다.
AWS CloudFront 아키텍쳐 - AWS공식 홈페이지
그럼 이런 업로드 과정을 어떻게 할것인가가 중요포인트인데 크게 2가지 방법이 있다.
이 방법은 제일 직관적인데 직접 리사이징된 이미지를 구해서 미리 정의한 버킷의 폴더로 업로드하는것이다.
이 방법이 직관적이지만 만약 회사에서 배포중인 서비스가 많고 개발할때 마다 이미지를 리사이징하여 직접 업로드하는 방식은 개발자 경험에는 좋지 않을것이다.
그래서 이부분을 자동화하면 더 좋아질 것이다.
이를 자동화하기 위하여 WebHook을 사용할수도 있다.
매번 배포되야하는 브랜치에 푸시될때마다 WebHook을 사용하여 원하는 만큼의 이미지를 리사이징하여 S3에 업로드한다.
AWS 람다는 아마존이 아마존 웹 서비스의 일부로서 제공하는 사건 기반 서버리스 컴퓨팅 플랫폼이다. 이벤트에 대응하여 코드를 실행하는 컴퓨팅 서비스이며 해당 코드에 의해 필요한 컴퓨팅 자원을 자동으로 관리해준다.
더 자세한 방법에 대해서는 정말 잘 설명된 블로그를 첨부하겠다.
AWS Lambda Image Resize 도입기 - 올리브영
이미지 최적화 얘기를 하는데 뜬금없이 왜 프론트엔드 프레임워크를 얘기하는가에 대한 의문이 들 수도 있다.
이미 아시는분이 더 많겠지만 NextJs에서 Image라는 내장 api로 이미지 최적화를 제공한다.
그 전에 우선 NextJs의 배포과정을 먼저 알아야 할 필요가 있다.
일반 React로 SPA를 구현해서 CSR하는 과정이라면 빌드된 소스코드 결과물을 S3에 업로드하고 유저들은 이 결과물을 받아서 클라이언트에서 렌더링 할 것이다.
하지만 NextJs와 같은 SSR이 가능한 프레임워크에서는 방법이 조금 다르다. 필자의 회사에서는 docker로 이미지를 띄워서 AWS에서 서버를 돌리는 방식으로 진행중이다.
Docker를 이용하여 NextJs 결과물 이미지를 빌드하고 이를 구동할 서버를 위해 AWS EKS나 EC2로 서버를 구동한다.
Docker로 이미지를 빌드할 때 nodemodules 결과물을 캐싱하여 배포 시간을 단축 할 수도 있다.
그러면 SSR로 개발되는 페이지는 사용자가 해당 컴포넌트에 접근했을 때 서버에서 html과 청크화된 JS 파일을 내려줘 Client에서 하이드레이션 하여 결과물이 사용자에게 보여지게 된다.
정리하자면 필요한 요청을 NextJs를 띄운 서버에서 처리하여 결과물을 내려준다.
여담으로 필자는 App라우터 기반의 NextJs 13버젼 이상을 사용하고 있는데 이때부터 React 18버젼에서 나온 RSC(ReactServerCompnent)를 이용하여 RSC Payload형태로 내려준다. 더 궁금하면 아래 공식 블로그글을 읽어보길 바란다.https://nextjs.org/docs/app/building-your-application/rendering/server-components#how-are-server-components-rendered
왜 이런 과정을 설명했냐하면 NextJS에서 이미지를 최적화하여 주는 방식을 알기 위함인데 NextJs는 이미지 최적화를 Sharp라는 node.js기반 API로 프로덕션 레벨에서 빌드하기를 권장한다. Default로는 Squoosh를 사용한다.
즉 개발자가 Image태그를 사용하여 코드를 작성하면 우리가 서버를 띄워놓은곳에서 sharp라는 api를 이용하여 이미지를 리사이징하여 이때 생성된 이미지를 유저에게 내려주게 된다.
NEXT.JS의 이미지 최적화는 어떻게 동작하는가? - 올리브영
이 과정을 서버 자원을 사용하여 진행하게 되고 이는 사용자가 많을수록 서버쪽 부하가 심해진다. 물론 로드밸런서를 이용하여 부하가 늘어났을때 서버쪽 자원의 한계치를 증가시킬 순 있지만 이 또한 비용이다.
정리하자면 NextJs에서 Image태그로 이미지 최적화를 할때엔 NextJs내부적으로 sharp라는 nodejs기반의 api를 사용하고 이를 서버자원에서 할당하여 사용하므로 서버쪽 부하가 생긴다. 즉 공짜가 아니다.
S3 가격은 버킷에 업로드된 용량에 대한 비용과 요청횟수에 대한 비용으로 책정하고 AWS EKS는 서버 구동에 대한 패키지에 따라 월간 비용을 책정한다.
그래서 이를 사용하려면 서버 부하가 크게 들지 않는 작은 서비스거나 서버 운영 비용보다 위에서 설명한 S3업로드 비용이 더 클때인데 큰 서비스일수록 일반적으로는 S3에 직접 업로드하여 이를 serving하는 비용이 더 저렴하다.
이를 어느정도 해결하기위해 Image의 loader 프로퍼티를 사용할 수 있다. 요청 URL에서 우리의 CloudFront URL을 붙일 수 있는데 이를 통해서 한번 리사이징된 데이터를 2번째 사용자가 요청한다면 캐시된 데이터를 serving할 수 있다.
자세한 건 아래 NextJs공식 블로그글을 참조 바란다.
https://nextjs.org/docs/pages/building-your-application/optimizing/images#loaders
NextJs 배포할때 Sharp Library를 직접 설치해줘야하는 문제도 있고 관련한 카테고리 글도 있다.
https://nextjs.org/docs/messages/install-sharp
그래서 결론은 이미지 최적화를 S3에 업로드하여, 요청되는건에 한해서 CloudFront에서 캐싱하여 사용자에게 빠른 속도로 serving할 수 있으며, NextJs에서도 최적화가 가능하지만 최적화기능에 대한 부하는 배포된 프론트서버에서 처리한다.
사용자가 서비스를 이용할 수 있는 코드를 작성하는건 프론트엔드 개발자의 영역이지만 이를 어떻게 잘 보여주고 그 과정에서 서비스의 물적 리소스를 낮추는 비용은 데브옵스이며 프론트엔드개발자도 이를 잘 알아야 할 필요성을 알게 됐다.
프론트엔드개발자로서 개발자끼리 협업을 잘 하기위하여 코드레벨에서의 최적화, 추상화도 매우 중요하지만 이를 사용자에게 서빙하기 위한 운영에서의 최적화 또한 중요하다.
그리고 둘 다 알고 있어야 진정한 최적화를 할 수 있다.
좋은것만 있는것처럼 보이지만 최적화는 공짜가 아니다. 이를 관리하기 위한 개발자 자원이 들어갈것이고 제일 좋은건 필요하지 않으면 하지 않는것이다.
본인이 운영하는 서비스에서 어떤게 제일 중요한지 판단할 줄 알아야하고 필요한것을 챙겨야한다. 최적화가 중요하지만 개발 하는 회사나 서비스에서 현재 개발자가 하는작업은 모두 리소스이고 이를 적절하게 잘 사용하는것이 중요하다.
필자가 개발한 서비스에서 이미지를 최적화 했던 이유는 해당 플랫폼이 유저 조회수가 많은 블로그 서비스였기 때문이다.
YAGNI(You aren't gonna need it) 프로그래밍 3대 기법중 하나인 "필요한 작업만 해라"에 대해서 잘 생각해볼 필요가 있다.
그리고 이번 블로그 포스팅에서는 이미지최적화만 다뤘는데 웹 개발에서 최적화하는 방법은 많고 이를 알려주는 강의 또한 많다. 혹시나 기회가 된다면 전반적으로 웹을 최적화 하는 방법에 대해서도 이야기해보려고 한다.