웹 환경 적응기

들어가며

최근에 Express와 Vue를 사용하는 웹 프로젝트를 하나 끝냈다. 그래서 그 회고를 적지 않을 수 없다.

학교에서(정확히 말하자면 앱센터) 카페테리아라는 서비스를 운영하고 있다. 이 서비스의 비즈니스 룰과 기타 파라미터들을 컴파일 없이, 원격 쉘 접속 없이, 데스크탑 없이 웹 콘솔로 읽고 수정할 수 있게 해주는 프로젝트였다.

우리에게 필요한 것은 사용자의 기기에서 어떠한 로직을 실행하며, 서버와 통신을 하는 어떠한 소프트웨어었다. 서버는 그냥 API 서버를 만든다 치고, 클라이언트는 플랫폼별 네이티브 애플리케이션과 웹 페이지라는 선택지가 있었다. 그중 웹을 선택했다. 그 호환성과 캐주얼함 때문이었다.

웹 vs 네이티브

웹(월드 와이드 웹)은 태생부터 크로스 플랫폼이다. 웹이 예전의 단순 문서 공유 시스템에서 오늘날의 Single Page App까지 발전하며 수많은 변화를 거듭하였지만, 웹 표준이 존재하며 브라우저에서 실행되는 모든 코드는 그 표준을 지켜야 한다는 사실에는 변함이 없다. 즉, 한 번 작성하면 어디서나 사용할 수 있다.

네이티브 앱은 아주 오래된 개념이다. 웹 앱과 비교하기 위해 만든 말이지만, 그냥 통상의 컴퓨터 프로그램을 지칭한다. 리눅스의 커맨드라인 유틸리티, 윈도우즈의 데스크탑 애플리케이션, iOS와 Android의 모바일 앱 등이 있다. 이들은 플랫폼의 운영체제와 API에 종속된다.

네이티브 앱은 하드웨어와 파일 시스템에 쉽게 접근하거나 플랫폼 API를 제대로 활용할 수 있다. 또한 사용자에게 친숙한 UX를 제공한다. 대신 아주 귀찮다. 바닥부터 UI를 쌓아 올리고 그 플랫폼이 요구하는 언어로 best practice를 따라 앱을 짜야 한다. 때로는 코루틴이나 스레드를 동원해 비동기 처리를 해야 하고, 네트워크 요청 하나 보내는 데에도 이것저것 요구하는 것이 많다(고속 개발에 익숙한 지극히 현대적 관점에서다).

반면 웹 앱은 훨씬 개발과 사용 측면 모두에서 캐주얼하다. 네이티브 앱이 CPU를 직접 지휘하는 거라면 웹 앱은 가만히 누워서 브라우저에게 이것 저것 시키는 것 같은 느낌이다. 일단 브라우저가 엄청나게 많은 일들을 해준다. 예를 들어, 어떤 주소에서 리소스를 가져오고자 한다면 아무 설정도 필요 없다. fetch를 사용하면 된다. 또한 브라우저는 튼튼하다. 예외가 터져도 그 기능만 문제가 될 뿐 앱이 죽거나 하진 않는다. 인터넷 상에서의 정보 교환을 통한 리소스 열람/수정이라는 목적에 맞게 특화된 것이다. 서비스의 설정을 읽고 변경해야 하는 시나리오에 딱 맞아떨어진다.

첫 웹 앱 개발

복잡한 클라이언트 로직을 가지는 웹 앱을 바닥부터 만드는 것은 어렵다. 그래서 프레임워크에 대한 수요가 많다. 그런데 이쪽 분야에는 오래된 강자가 없다. iOS는 애플이 주도한다. Android는 Google이 주도한다. 웹 프론트엔드는? 지배적인 위치를 점한 주체가 없다. React, Vue, Angular 등이 점유율을 다투고 있다.

웹 프론트엔드는 발전이 아주 빠르다. 자고 일어나면 새로운 라이브러리가 나와있고, 해마다 새로운 언어와 프레임워크가 나온다. 상황이 이렇다 보니, 있는 것 중 프로젝트에 잘 맞고 내 맘에 드는 것을 잘 찾아다 쓰는 것이 아주 중요하게 되었다.

이번 프로젝트에서 서버는 Express를 사용하였다. 각 라우트별로 미들웨어 체인을 이어붙이는 접근 방식이 마음에 들어서였다.

프론트엔드 쪽은 Vue를 썼다. React와 Vue 사이에서 고민했는데, 둘 다 안 써본 상태였고, React는 너무 유명한 것 같고 Vue는 이제 막 떠오르는 느낌이라 왠지 마음이 가서 선택했다. 딱히 별 이유는 없다.

컴포넌트 라이브러리(UI)는 구글이 밀어주는 Material을 구현하는 Vuetify를 가져다 썼다. 당시 검색하다 처음으로 보인 거라(…) 선택했다. 왜 Material이었냐면, 안드로이드 앱 개발을 막 끝낸 참이라 친숙해서 그랬다.

개발하다가 노트북 뚜껑 닫아버리고 싶던 순간들

새롭게 뛰어든 곳에서는 삽질을 안할 수가 없다. 여러 이유로 고통을 받았는데, 기억 나는대로 짧게 언급해보겠다.

망할놈의 CORS

웹 프론트엔드를 정적 호스팅하는 서버와 백엔드를 담당하는 API 서버를 분리했다. 그랬더니 클라이언트에서 요청을 보낼 수가 없었다. 알고 보니, API 서버에서 클라이언트의 요청을 허락해 주어야만 하는 거였다. 그러니까, 웹 앱을 https://myweb.com에서 호스팅하고 있으면, 서버가 응답 헤더에다가 Allow-Control-Allow-Origin=https://myweb.com(또는 와일드카드)을 넣어서 보내야만 한다는 것이었다.

그런데 웃기는 것이, 요청을 허락하고 말고를 결정하는 로직은 브라우저에 들어있다는 것이다. 그렇기 때문에 프록시 서버를 둔다든가 하여 응답 헤더를 조작하기만 하면 문제 없이 요청이 가능해지는 것이었다. 서버 응답에 따라 브라우저가 내 코드를 막아버린 것. 브라우저는 내 편이 아니었다.

서버와 내 코드 사이에 브라우저가 끼어들어 마치 운영체제처럼 기능하는 것이었다. 내 코드의 실행을 통제하는 플랫폼으로써의 브라우저의 존재를 확실히 느끼게 되었다.

쿠키가 와도 자꾸 block을 해

개발 환경에서, 웹페이지는 http://10.0.1.10:9090(내컴퓨터), API 서버는 http://localhost:8081에 띄워두고 테스트를 했다. 이미 CORS 이슈를 겪은 터라 웹페이지 origin은 허용해 둔 상태였다. 그런데 로그인을 해도 쿠키가 안 생겼다.

분명 응답 헤더를 보면 Set-Cookie가 있었다. 값도 잘 들어 있었다. 그런데 브라우저에 쿠키가 생기지 않았다. 미칠 노릇이었다. 그나마 크롬은 친절해서 나에게 그 이유를 알려 주었다.

도메인이 완전히 다른 서버에서 쿠키를 설정하고자 하면 same-site=none을 해야 하는데 그게 없어 기본 설정인 same-site=lax로 간주되었고, 따라서 다른 사이트의 쿠키를 block했다는 거다. 그래서 서버 쪽에다가 쿠키를 설정할 때에 same-site=none 옵션을 주도록 했다.

그러자 증상은 같았고 브라우저의경고 메시지가 바뀌었다. same-site=none을 할거면 secure 옵션이 들어가야 한단다. 아… 해당 옵션을 주면 https 상에서만 쿠키가 설정된다. 개발 환경에서 그럴 수는 없은 노릇인 것이다.

결국 개발 환경에서 도메인을 동일하게 하는 방법을 택할 수밖에 없었다. 포트는 달라도 도메인은 같게 해야 했다.

배포를 해도 예전 버전이 그대로

프론트엔드는 트랜스파일을 거쳐 AWS S3에 올리고 Cloud Front로 호스팅했다. 이게 프로덕션 급의 참 훌륭한 CDN인건 맞는데 전파 속도가 좀 속터지게 느렸다. 하나 배포하고 결과를 좀 보려고 하면 몇 시간씩 걸렸다.

그렇다고 로컬에 띄워두고 테스트하려니 위에서 언급한 것처럼 localhost와 API 서버의 도메인이 완전히 달라 쿠키도 설정되지 않았다. 때문에 로컬에서 API 서버까지 같이 띄울 수밖에 없었다(배포된 API 서버에서 해결할 수도 있었지만 귀찮아서 안 했다).

결국 Netlify로 갔다. 배포도 엄청 편하고(그냥 GitHub에 푸시만 하면 배포가 된다) 인증서도 알아서 관리해주고 전파도 엄청 빨랐다.

결과

아주 만족스러운 결과물이 나왔다. 처음에 목표했던 대로 ‘누워서 폰으로 서비스 설정 건드릴 수 있는 환경’을 구축하는 데에 성공했다.

개발 기간은 2주 남짓 걸렸던 것 같다. 아주 짧은 기간이다. Node & Express & MySQL 기술 스택에 익숙했고, Vue도 예전에 한 번 아주 조금 이해해본 적이 있었기에 가능했다. 물론 프로젝트 규모 자체가 작았기 때문이기도 하다.

마치며

다양한 기술을 접하며 두려움을 조금씩 없애가고 있다. 언젠가는 iOS 앱을 다뤄보고 싶다.

댓글