나만무 프로젝트의 구현 사항 중, '검색을 통한 이미지 노드 생성'이 있었다. 이 부분은 내가 구현하게 되었는데, 구현 자체만을 떼 놓고 본다면 난이도는 어렵지 않았다. 무료 이미지 사이트인 Unsplash
API를 이용했었는데, 워낙 사용하기 편하게 잘 되어있어서 😀 이 땐 몰랐다. 이미지 노드가 우리 팀을 얼마나 속썩일지..
SOMETHINK
의 기능 중에는, 작업한 마인드맵을 이미지 파일로 저장할 수 있는 캡처 기능이 있다. 따로 DB에 세션의 마인드맵 정보를 저장하지 않기에, UX적으로 필수적인 기능이다. 그런데, 잘 작동하던 마인드맵 캡처가, 어느 순간부터 먹통이 되는 경우가 생겼다. 어...? 대체 어떤 때 문제가 생기나 테스트 해 본 결과, 원인은 아니나 다를까 이미지 노드 😂
이미지 노드를 추가하기만 하면, 흰 화면만 캡처가 된다! 대체 왜?! 바로 보안 정책 위반으로 캔버스가 오염되었기 때문이다. 이 때 CORS와 동일 출처 정책의 존재를 처음 알게 되었다. 검색해 보니 초보 웹 개발자들은 한번은 만나는 이슈라고 하더라고 🥹
CORS
는 대체 뭐길래 이렇게 신입 개발자들을 괴롭히는걸까? 어떤 녀석인지부터 알아보자.
CORS가 무엇인지 알아보기에 앞서, 동일 출처 정책이 무엇인지 알 필요가 있다.
동일 출처 정책이란, 어떤 출처에서 불러온 문서나 스크립트가 다른 출처에서 가져온 리소스와 상호 작용할 수 있는 방법을 제한하는 중요한 보안 메커니즘이다.
보안 상의 이유로 웹 브라우저는 이러한 동일 출처 정책을 따른다. 예를 들어 Fetch
API를 통해 데이터를 가져오고자 한다면, 자신의 출처와 동일한 리소스만 불러올 수 있다.
그렇다면, 정녕 웹 브라우저는 다른 출처의 자원을 다룰 수 없는걸까? 당연히 그렇지 않다. 이제, CORS에 대해 알아보자.
CORS(Cross-Origin Resource Sharing)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
서버 측에서 HTTP 요청에 CORS 헤더를 설정하여 송신하게 되면, 해당 리소스에 접근이 가능해지는 것!
CORS와 보안 정책에 대해 이제 알았으니, 문제를 해결해 볼 시간! 우선 개발자 도구를 열어, API를 통해 가져온 이미지의 CORS 헤더부터 확인해보았다.
*
라면, 이는 모든 출처에서의 요청이 서버로부터 응답을 받을 수 있음을 의미한다. 즉, CORS정책을 완전히 허용한다는 뜻!가져온 이미지 자체에는 문제가 없음을 확인했다. 그렇다면 대체 언제 캔버스가 오염되는 것일까? 추측해볼 수 있는 건 하나, 불러온 이미지를 이미지 노드로 변환하는 과정에서 보안 정책을 위반하는 건 아닐까?
그렇다면 문제가 복잡해지기 시작한다. 라이브러리 아래 단에서 어떻게 이미지가 노드로 변환되는지 정보가 일절 없다. 그렇다고 라이브러리를 들어내고 기반을 뜯어고치거나, 라이브러리 코드를 다 뜯어본다? 그럴 시간은 당연히 없다. 아마 이쯤부터 팀 분위기가 심각해지기 시작했다 😨
고군분투 끝에 우리 팀이 찾아낸 해답은 프록시 서버를 통해 애초에 동일 출처 정책 자체를 회피하는 것이었다. 메인 서버 이외에 별도에 프록시 서버를 구축하고, 프록시 서버에서 이미지 API요청을 처리하여 결과를 클라이언트에게 전달한다.
이런 방식을 취함으로써 CORS 헤더를 별도로 수정하지 않고도 보안 정책을 회피할 수 있었고. 정~말 다행히 문제 없이 잘 동작했다. 중간 발표를 앞두고 만난 문제였기에 더더욱 우리를 힘들게 했던 문제가 아닌가 싶다.
개발 프로세스 내내, 점점 우리 페이지가 무거워진다는 생각을 팀원 모두가 하긴 했다. FE의 규모가 커지니까 성능 저하는 피할 수 없구나! 하며 대수롭지 않게 여긴 것이 화근이었다.
개발 프로세스 말미, 이제는 개발보다 발표 시나리오와 구성에 더 신경쓰고 있을 때였다. 근데 뭔가 이상했다. 컴퓨터 성능이 낮은 팀원들이 시연이 불가능 할 정도로 사이트가 느려지더니, 급기야 Out of Memory
에러를 뱉으며 브라우저가 강제 종료되기까지 하는 것!!! 엑?!!
이때부터 심상치 않음을 느낀 우리는 페이지 성능을 테스트해보기 시작했고, SOMETHINK
의 충격적인 메모리 점유율을 마주하게 된다. 2GB? 3GB는 우습게 뛰어넘는, 또 계속 높아지는 메모리 점유율을...
심각성을 느낀 난, 부랴부랴 개발자 도구를 통해 이 정신나간 메모리 점유가 어디서 일어나는지 알아내려 했다. 이미 눈치채셨겠지만 원인은 이미지 노드 😂 또 너야?!
사실 팀원 모두가 이미지 노드가 원인임을 어렴풋이 짐작하긴 했다. 이미지 노드를 추가하면 눈에 띄게 성능이 저하되는 것을 봤으니.. 그럼에도 불구하고, 원인을 확인하고는 두 눈을 의심할 수 밖에 없었다. 정신나간 부하의 원인은 바로 장장 20만자 가까이 되는 이미지 URL였다. 이런 말도 안되는 분량의 데이터를 실시간으로 서버에 전송하고 받아오고 하니, 당연히 말도 안되는 수준의 부하가 발생했던 것. 대체 이유가 뭘까?
SOMETHINK
프로젝트의 base64
인코딩 코드문제의 원인은 바로 base64
인코딩이었다. 간단하게 말하면 이 친구는 이진 데이터를 텍스트 형태로 변환하는 인코딩 방식인데, 처음 코드를 작성할 때는 별 생각 없이 이미지를 이 방식으로 인코딩하여 결과를 전송했다.
요점은 이진 데이터를 변환하여 텍스트로 바꿔준다 라는 점이다. 방대한 픽셀 데이터가 존재하는 이미지 파일을, 생으로 텍스트로 바꾼다? 앗.. 당연히 데이터 분량이 엄청나게 커지겠구나.. 이제서야 퍼즐이 맞춰지는 기분이었다.
이러한 문제를 어떻게 해결할 지 많은 고민이 있었다. 프록시 서버를 아예 파기하고 직접 API를 호출하자니 캔버스가 오염되어 화면 캡처가 안되고, 그렇다고 그냥 두기에는 성능에 치명적인 부하가 발생하고 😂
아무리 그래도 이 정도의 성능 이슈를 방치할 수는 없었다. 데이터량을 줄일 방법을 찾아보고, 이것저것 적용해본 끝에 두 가지 해결 방법을 통해 문제를 해결할 수 있었다.
첫 번째 방법은 바로 WebP
이미지 포맷을 통한 데이터 압축이다. WebP
는 구글에서 똑똑하신 분들이 만든 이미지 포맷인데, 기존의 JPG
, PNG
와 같은 노후화된 방식보다 작은 파일 크기에, 고품질의 이미지를 제공해준다. WebP
방식은 명성에 걸맞은 성능을 내 주었고, 눈에 띄게 데이터량을 줄여줬다.
두 번째 방법은 바로 이미지 다운스케일링이다. 이미지의 품질을 희생하여 데이터량을 줄여내는 방법인데, 핵심 요지는 우리 프로젝트에서 각각의 이미지가 사용되는 곳은 작은 이미지 노드들이고, '사용자 측면에서 초 고품질의 이미지는 아무런 메리트가 없다는 것'이다! 따라서 다양한 이미지에서 '유저 측면에서 거슬림 없는 정도' 의 품질을 테스트해보고, 해당 품질까지의 다운스케일링을 진행했다.
이러한 두 가지 방법을 통하여 데이터량을 평균 20만 자 -> 2~3천자 로 줄여냈다. 실로 믿기지 않는 데이터량 다이어트량! 개선 이후에는 기존에 발생했던 문제는 말끔히 해결! 이미지를 백개 넘게 띄워도 버벅임이 없어졌다.
프로젝트를 진행하면서 겪은 문제들은 대부분 나비효과였다. 별 생각 없이 구현했던 작은 기능들이, 시간을 지나 내 실력으론 손쓰지 못할 정도로 어려운, 거대한 문제로 돌아왔다. '프로젝트 기간이 타이트했으니까 어쩔 수 없었지~' 라고 치부하기엔, 안일하게 넘겼던 부분이 분명히 존재했다.
결국 이 역시 답은 정해져있다. 내가 사용하려는 것, 그것이 기술 스택이던 무엇이든 간에 사용하려는 것에 대해 확실히 알고 사용해야 한다는 것! 지금에서야 어떻게 해결 할 수 있는 문제로 돌아왔지만, 차후 큰 규모의 프로젝트를 진행한다면 걷잡을 수 없는 문제를 만날 수도 있다. 다시 한번 마음에 새기고, 더더욱 좋은 개발자를 향해 정진하자. 오늘도 화이팅~~