(번역) 아주 거대한 (자바스크립트) 어플리케이션을 구축하기

이 글은 Malte Ubl의 "Designing very large (JavaScript) applications"의 번역입니다. 
이해를 돕기 위해 원문에는 없는 소제목을 붙였으며 가독성을 떨어뜨리던 원문의 이미지들 중 일부를 의도적으로 첨부하지 않았고, 수많은 의역이 들어갔습니다.
이 글의 모든 권리는 원 저작자에게 있습니다만 번역하는 데에도 아주 고생했습니다. All rights are reserved to the original author.

이 글은 호주 JSConf에서의 제 발표 스크립트를 약간 편집한 것입니다. 유투브에서 전체 발표를 볼 수 있습니다.

들어가며.

안녕하십니까. 저는 아주 거대한 자바스크립트 앱을 구축해왔습니다. 이젠 이런 일을 더이상 하지 않기 때문에, 제가 해온 일을 돌아보면서 무엇을 배웠는지 공유할 좋은 타이밍이라고 생각합니다.

어제 학회 파티장에서 맥주를 마시며 질문을 하나 받았는데요. “Malte, 당신이 이 주제에 대해 이야기할 만한 자격이 있다고 왜 생각하시나요?” 이 질문에 대답하는 것이 실제로 제 발표에 연관이 있다고 생각하기 때문에, 저 자신에 대해 이야기하는 게 익숙하지는 않지만 해보겠습니다.

저는 구글에서 자바스크립트 프레임워크를 만들었습니다. 그 프레임워크는 Photos, Sites, Plus, Drive, Play, 검색엔진 등 구글이 제공하는 서비스들에 사용되고 있죠. 개중 몇몇 사이트는 꽤 크고, 당신도 아마 몇 개는 써본 경험이 있으실 겁니다.

이 프레임워크는 오픈소스는 아닙니다. 그 이유는, 이 프레임워크가 React랑 비슷한 때에 나왔고, 제가 “사람들이 선택할 만한 자바스크립트 프레임워크를 더 늘릴 필요가 있을까?”라고 생각했기 때문입니다. 구글은 이미 오픈소스 자바스크립트 프로젝트를 몇 개 내놓은 상태였고(Angular와 Polymer), 또 하나가 추가되면 사람들이 혼란을 겪으리라 생각했기에, 그냥 우리끼리만 가지고 있자고 결정했습니다. 하지만 오픈소스가 아니었더라도, 이걸 만든 경험에서 배울 점이 많을 뿐더러 우리가 배운 걸 공유할 가치가 충분히 있다고 생각합니다.

거대한 앱들의 공통점은 혼자 만드는 게 아니라는 것이다.

이제 아주 거대한 앱에 대해, 그리고 그런 앱들이 가지는 공통점에 대해 이야기해봅시다. 거대한 앱은 그걸 개발하는 사람도 많겠죠. 수십 명일 수도 있고 더 많을 수도 있습니다. 당신은 이들에게 감정도 있고, 다른 사람들과의 사이에 문제도 있을 수 있다는 점을 염두에 두어야 합니다.

팀이 그렇게 크지 않더라도 여러가지 문제가 생길 수 있습니다. 당신이 그 앱을 개발하는 데 꽤 오랜 시간을 투자해왔을 수도 있고, 그 앱을 유지보수하는 첫 번째 사람이 아닐 수도 있고, 모든 컨텍스트를 가지지 않았을 수도 있고, 당신이 완전히 이해하지 못한 무언가가 있을 수도 있고, 팀 내의 누군가는 앱 전반에 대해 이해하지 못하고 있을 수 있습니다. 이런 게 우리가 아주 커다란 앱을 구축할 때 생각해야 할 사항들입니다.

시니어의 다음 단계로 나아가는 길은 ‘공감’에 있다.

img
트윗: 주니어 개발자 없는 시니어 개발자 팀은 그냥 개발자 팀과 같다.

이 자리에서 얘기하고 싶은 또 하나의 주제는 커리어입니다. 여기 계신 많은 분들은 스스로가 시니어 개발자라고 생각하고 계실 겁니다. 또는, 아직은 시니어가 안 됐지만 되기를 원하고 있겠죠.

저는 시니어가 된다는 것의 의미를, 제게 주어지는 거의 모든 문제를 해결할 수 있는 능력이 있는 것으로 생각합니다. 적절한 도구와, 적절한 도메인 지식을 알고 있으니까요. 그리고 또 하나의 중요한 의미는, 주니어 개발자가 언젠가 시니어가 될 수 있도록 돕는 것입니다.

하지만 어느 순간에 우리는 “그럼 그 다음 단계는 뭐지?” 하고 의문을 가지게 됩니다. 시니어가 되고 나면 그 다음은 어디로 가야 할까요? 누군가는 ‘관리자’라고 대답하겠지만 그게 모두에게 적절한 답은 아닙니다. 모든 사람이 관리자가 될 순 없으니까요. 어떤 사람들은 정말 훌륭한 개발자인데, 여생동안 개발자로서 일하는 게 안될 게 뭡니까?

img
‘나’는 ‘내’가 어떻게 문제를 풀어야 할지 안다.

저는 시니어의 다음 단계로 가는 길을 하나 제시하고자 합니다. 시니어 개발자로서, 저는 “나는 내가 어떻게 문제를 풀어야 할지 안다”고 당당히 말할 수 있습니다. 그리고 제가 어떻게 문제를 풀지 알기 때문에, 저는 다른 사람이 어떻게 문제를 풀지 가르쳐줄 수도 있습니다.

img
‘나’는 ‘다른 사람’이 어떻게 문제를 풀어야 할지 안다.

따라서 제가 생각하는 시니어의 다음 단계는, 스스로 “나는 다른 사람이 어떻게 문제를 풀어야 할지 안다”고 당당히 말할 수 있는 것입니다.

조금 더 구체적으로 말해보면 이렇습니다. “나는 내가 선택하는 API 또는 내가 프로젝트에 도입한 추상화가, 다른 사람이 문제를 푸는 데 어떤 영향을 미칠지 예상할 수 있다.” 저는 이 문장이, 내 선택이 앱에 어떤 영향을 미칠 것인지 생각해보게 하는 강력한 도구가 된다고 생각합니다.

이걸 ‘공감하며 앱 만들기(An application of Empathy)’ 라고 부를 수도 있습니다. 당신의 행동 하나하나와 당신이 다른 개발자에게 주는 API 하나하나가, 그들이 소프트웨어를 개발하는 데 어떤 영향을 미칠 것인지 생각해보는 것입니다.

다행히 이런 공감은 쉬운 수준에 속합니다. 공감하는 건 일반적으로 어렵고, 공감하며 앱 만들기도 결코 쉽진 않습니다. 하지만 당신히 공감해야 할 사람들은 적어도 소프트웨어 개발자일 것입니다. 그들이 당신과 많은 점이 다르더라도 모두 소프트웨어를 개발한다는 공통점이 있습니다. 당신이 경험을 쌓으면 쌓을수록 이런 공감은 더 잘 할 수 있게 됩니다.

생각하는 방식(=프로그래밍 모델)을 변화시키는 결정은 신중해야 한다.

공감에 대해 생각할 때 제가 이야기하고 싶은 아주 중요한 주제는 ‘프로그래밍 모델’입니다. 프로그래밍 모델은 “API, 라이브러리, 프레임워크, 도구 등 주어진 조건 하에서 사람들이 어떻게 소프트웨어를 개발하는가”를 뜻합니다. 사실 이 발표는 API와 기타 등등에 준 조그마한 변화가 프로그래밍 모델에 어떤 영향을 끼치는지에 대한 이야기이기도 합니다.

img
프로그래밍 모델에 영향을 미치는 예: React, Preact, Redux, NPM에서 가져온 날짜 선택 라이브러리, NPM

무엇이 프로그래밍 모델에 영향을 끼치는지 몇 가지 예를 들어보죠. 당신이 Angular로 만들어진 프로젝트를 진행 중이고, “이제 이걸 React로 바꿔야겠어”라는 결정을 한다면, 이는 분명 사람들이 소프트웨어를 어떻게 작성하는지에 영향을 줄 겁니다. 하지만 “겨우 가상 DOM 다루는 데 60KB를 쓴다고? Preact로 바꾸자”라는 결정은, (Preact는 API 호환이 되는 라이브러리이기 때문에) 사람들의 소프트웨어 작성 방식을 바꾸지는 않을 것입니다.

그러고 나서 “앱이 너무 복잡해. 앱 동작을 조절하는 뭔가 필요하니 Redux를 쓰자”라는 결정을 내린다면, 이건 영향을 미칩니다. 이제 날짜 선택(Date picker) 기능이 필요해서 npm에서 검색해보니 결과가 500개 나왔고, 그 중 하나를 골랐다고 칩시다. 무엇을 골랐는지가 정말 중요할까요? 이건 소프트웨어 작성 방식을 변화시키진 않을 겁니다. 그러나 npm이라는 거대한 모듈의 집합을 능숙하게 사용할 줄 안다는 것은 분명 소프트웨어 작성 방식에 지대한 영향을 미칩니다. 물론 이것들은 겨우 몇 가지 예시일 뿐입니다.

좋은 프로그래밍 모델은 결정을 사람이 아닌 시스템에 맡긴다.

주제를 약간 바꿔서, 거대한 자바스크립트 앱을 사용자에게 배포하려 할 때 공통적으로 가지게 되는 문제에 대해 이야기해봅시다. 그 문제란 앱의 크기가 일정 이상 커지고 나면, 앱을 사용자에게 한꺼번에 전달하고 싶지 않아한다는 것입니다. 그래서 코드 분할을 도입하게 됩니다.

코드 분할이란, 앱을 번들(bundle, 코드 뭉치)의 집합으로 정의함을 뜻합니다. 즉 “어떤 사용자는 내 앱의 이 부분만 사용하고, 다른 사용자는 요 부분만 사용한다”는 것을 파악하고, 사용자가 실제로 실행하는 부분만 다운로드될 수 있도록 특정 코드를 묶어놓는 것입니다. 여기까지는 우리 모두가 할 수 있습니다. (자바스크립트 세계의 많은 기능들처럼 코드 분할도 클로저 컴파일러를 통해 만들어졌지만, 코드 분할을 하는 가장 널리 알려진 방법은 웹팩을 이용하는 것입니다. 당신이 RollupJS를 쓰고 있다면, 여기에서도 얼마 전부터 코드 분할을 지원하기 시작했다는 걸 알아두시기 바랍니다.)

코드 분할이 분명 우리 모두가 써야 하는 기술이긴 하지만, 당신의 앱에 이걸 도입하고자 할 때는 몇 가지 고려할 사항이 있습니다. 코드 분할은 프로그래밍 모델에 영향을 미치기 때문입니다.

img
동기 -> 비동기.

코드 분할을 하면, 원래는 동기적으로 작동했던 것들이 비동기화됩니다. 코드 분할을 하기 전까지 당신의 앱은 단순하고 멋졌습니다. 커다란 것 하나만 있었죠. 일단 로드되고 나면 안정적이고, 이해하기 쉽고, 기다릴 필요가 없었습니다. 그러나 코드 분할 이후에는 때때로 “저 번들이 필요해”라며 네트워크를 거쳐야 할 것이고, 이 사실을 염두에 두어야 하기 때문에 앱이 좀더 복잡해집니다.

또한, 우리는 사람을 고려하기 시작해야 합니다. 코드 분할을 위해선 번들을 정의해야 하고, 그러려면 언제 번들을 불러올지 생각해야 하죠. 따라서 사람들, 즉 당신 팀의 동료 개발자들이 이제 번들에 무엇이 들어가고 언제 그걸 불러올지 결정을 내려야 한다는 게 됩니다. 이러한 것들을 고려해야 하기 때문에, 사람을 연관시키는 결정은 언제나 프로그래밍 모델에 분명한 영향을 미칩니다.

img
경로 기반 코드 분할.

코드 분할에 있어서 사람을 복잡한 고려사항에서 빼버리는 좋은 방법으로 ‘경로 기반 코드 분할’이 있습니다. 아직 당신이 코드 분할을 하지 않고 있다면 아마 이 방법이 첫 번째로 시도해보기에 적절할 겁니다.

경로란 앱에서의 기본적인 URL 구조를 뜻합니다. 예를 들어 당신 앱의 /product/라는 경로 아래에 상품 페이지들이 있고, 다른 경로에 카테고리 페이지들이 있다고 해봅시다. 이 때 그냥 각 경로를 하나의 번들로 만들면 앱의 라우터가 코드 분할을 책임지게 됩니다. 사용자가 한 경로에 들어가면 라우터가 관련 번들을 불러올 것이므로, 해당 경로 안에서는 더이상 코드 분할을 신경쓸 필요가 없습니다. 당신은 다시 전체를 번들 하나로 두었던 옛 프로그래밍 모델과 거의 같은 방식으로 생각할 수 있게 되죠. 이와 같이 경로 기반 분할은 코드 분할의 첫 번째 단계로 아주 좋습니다.

안타깝게도 이 발표의 제목은 아주 거대한 자바스크립트 앱을 구축하는 것이었습니다. 앱이 너무 커져서 경로 하나하나조차 너무 커지면, 경로별로 하나씩 번들을 두는 게 적절치 않게 됩니다. 충분히 커다란 앱의 적절한 예를 하나 보여드리죠.

img
“public speaking 101”의 구글 검색 결과.

저는 이 발표가 다가오면서 어떻게 좋은 연사가 될 수 있을지 알고 싶었고, 이러한 멋진 푸른색 링크 목록을 얻었습니다. 여러분은 이 페이지가 경로 기반 번들에 아주 적절하다고 생각할 수도 있을텐데요.

img
“날씨”의 구글 검색 결과.

캘리포니아의 겨울이 상당히 추웠다는 걸 기억하고 날씨를 검색해보니, 갑자기 완전히 다른 모듈이 나타납니다. 그러니까 이 단순해 보였던 경로가 사실은 더 복잡했다는 것이죠.

img
“20 미국 달러는 호주 달러로 얼마인가”의 구글 검색 결과.

그리고 저는 이 학회에 초청되어 미국 — 호주의 달러 환율이 궁금해졌고, 이제는 복잡한 환율 계산 화면이 나타났네요. 이런 식의 전문화된 모듈이 수천 개는 있기 때문에, 이를 번들 하나에 몰아넣으면 용량이 수 메가바이트는 될 것이고, 사용자는 아주 불만족스러울 것입니다.

따라서 경로 기반 코드 분할을 그냥 쓸 수는 없고 다른 방법이 필요합니다. 경로 기반 분할이 좋았던 이유는, 앱을 가장 바깥쪽 단위에서 분할함으로써 그 안쪽에서는 코드 분할을 신경쓸 필요가 없었기 때문입니다. 저는 단순함을 좋아하니까, 가장 바깥쪽 대신 가장 안쪽에서 분할을 해보면 어떨까요? 웹사이트에 존재하는 모든 컴포넌트 하나하나를 지연 로딩한다고 가정해봅시다. 만약 사용자의 네트워크 대역폭 하나만 생각한다면 아주 효율적인 방법이 되겠죠. 응답시간 측면에서 보면 아주 안 좋겠지만, 적어도 고려할 가치는 충분히 있는 방법입니다.

하지만 예를 들어 당신의 앱이 React를 쓴다면 어떻게 될까요. React에서 컴포넌트는 자신의 자식 컴포넌트에 정적으로 의존합니다. 이는 당신이 자식 컴포넌트를 지연 로딩하면 React의 작동 방식이 변하게 되고, 프로그래밍 모델이 바뀌게 되고, 상황이 안좋아지게 된다는 뜻입니다.

자, 검색 페이지에 환율 계산기를 넣고 싶어졌다고 칩시다. import 를 하겠죠? 다음은 ES6 모듈을 import 하는 일반적인 방법입니다.

import CurrencyConverter from './CurrencyConverter';

이걸 지연 로딩하고 싶다면 아래와 같이 import 방법을 바꿔서 동적인 import를 해야 합니다. 저는 React 전문가도 아니고, 동적 import를 하는 수많은 방법이 있겠지만, 확실한 건 이렇게 함으로써 당신의 코드를 작성하는 방식이 달라진다는 것입니다.

const LoadableConverter = Loadable({
loader: () => import('./CurrencyConverter'),
loading: Loading,
});

상황은 이제 그다지 좋지 않습니다. 정적이었던 것이 동적으로 바뀌었고, 이는 프로그래밍 모델이 변한다는 또다른 신호입니다. 이제 갑자기 “무엇을 언제 지연 로딩할 것인지 누가 정하는지”가 궁금해지기 시작합니다. 이 문제가 앱의 응답속도에 영향을 미치기 때문이죠.

사람이 다시 끼어들어서, “정적 import도 있고 동적 import도 있는데 무엇을 언제 사용해야 하는가?”를 결정해야 합니다. 이 결정을 잘못하면, 동적으로 했어야 했던 걸 정적으로 import하면서 같은 번들에 들어가면 안 되는 것들이 들어갑니다. 오랜 기간동안 많은 개발자가 함께 일하다 보면 이런 잘못된 일들이 벌어지기 마련이죠.

구글은 코드 분할 문제를 어떻게 풀었는가.

이제 구글이 실제로 이 문제를 어떻게 풀었는지, 그리고 좋은 성능과 좋은 프로그래밍 모델을 동시에 가져가는 방법에 대해서 얘기해보겠습니다. 우리는 먼저 컴포넌트를 렌더링 방식에 따라 분할하고, 그 다음 앱 로직에 따라 분할했습니다. 여기서 앱 로직이란, 예를 들어 사용자가 환율을 의미하는 쿼리를 검색했을 때 환율 변환기가 나타나게 하는 등의 동작을 뜻합니다.

따라서 특정 컴포넌트에서 사용되는 앱 로직은 오직 그 컴포넌트가 렌더되었을 때에만 로드됩니다. 이 방식은 보기보다 매우 단순한데, 단순히 서버 측에서 페이지를 렌더해놓은 다음, 실제로 사용자에게 무언가가 렌더되면 관련된 앱 번들이 다운로드되도록 하면 되기 때문입니다. 즉 로딩이 렌더링에 의해 자동으로 이루어지기 때문에 사람이 생각할 필요가 없어집니다.

img
검색 결과 페이지의 환율 변환기.

서버측 렌더링이 이렇게 좋아보이긴 하지만 사실 반대급부도 있습니다. React나 Vue.js 같은 프레임워크에서 서버측 렌더링은 hydration이라는 과정을 통해 이루어집니다. hydration의 작동 방식은 서버측에서 무언가를 렌더한 다음 클라이언트에서 한번 더 렌더하는 것입니다. 즉 코드의 로딩과 실행 양쪽에서 낭비가 생기죠. 이렇게 하면 CPU와 네트워크 대역폭에서 손해가 있지만, 대신 서버에서 무언가 렌더링했다는 사실을 클라이언트에서 무시할 수 있다는 점이 아주 좋습니다.

이렇듯 거대한 앱을 구축할 때는, 더 복잡한 대신 로딩과 렌더링이 아주 빠른 방법을 쓸지, 아니면 덜 효율적이지만 좋은 프로그래밍 모델을 제공하는 hydration을 쓸지 결정해야 합니다. 구글에서는 hydration을 사용하지 않는 걸 선택했습니다.

(역자 주: React에서의 hydration은 단순히 클라이언트가 한번 더 렌더링하는 게 아니라, 서버가 렌더링한 마크업이 존재한다면 새로 클라이언트가 렌더하지 않고 이벤트 핸들러만 bind시켜서 첫번째 로딩이 빨라지게 만드는 방법입니다. 발표자의 말과 약간 차이가 있는 듯해 첨언합니다만, 제가 발표자의 의도를 정확히 이해하지 못했을 수 있습니다. 이 단락은 전체적으로 모호한 부분이 많아서 원본 유투브 영상을 보고 많은 부분을 의역했습니다. )

코드를 지우기 쉬운 구조로 만드는 것은 아주 중요하다.

img

제 다음 주제는 제가 컴퓨터과학 분야에서 가장 좋아하는 문제에 대한 것입니다. 아, 이름 짓기 문제는 아닙니다. 이미 이 문제를 안좋은 이름으로 지었을 수는 있겠습니다만… 문제의 이름은 “2017 특별 휴일 문제” 입니다. 여기 계신 분들 중 이제 더이상 쓰이지 않지만 여전히 코드베이스에 남아있는 코드를 작성해본 적 있으시다면 손을 들어보세요.

제 생각에 이런 일은 특히 CSS에서 많이 벌어지는 것 같습니다. 거대한 CSS 파일이 하나 있고, 셀렉터도 하나 있습니다. 당신의 앱 어디에 이 셀렉터가 여전히 쓰이고 있는지 대체 누가 제대로 알 수 있을까요? 그래서 결국 그 셀렉터를 그대로 놔두게 됩니다. 최근에는 이런 문제를 자바스크립트 안에 CSS를 작성하는 방법(CSS-in-JS)으로 해결하는 듯합니다. 이 방법을 쓰면, 2017HolidaySpecialComponent라는 컴포넌트를 이제 2017년이 아니라는 이유로 지우게 될 때 모든 게 함께 지워집니다. 코드를 지우기 아주 쉬워지는 거죠. 저는 이게 엄청나게 좋은 아이디어이고, CSS뿐만 아니라 다른 곳에도 적용되어야 한다고 생각합니다.

이와 같이 설정을 한 곳에 모아두면 코드를 삭제하기가 아주 어려워지기 때문에, ‘중앙 설정’이 만들어지는 것을 최대한 피하는 것이 좋습니다. 몇 가지 예를 들어보겠습니다.

나쁜 예 1: routes.js

앞에서 경로에 대한 얘기를 했었는데요. 많은 앱들이 routes.js같은 파일에 앱에서 사용하는 모든 경로를 정의해놓고, 거기서 다른 컴포넌트를 불러오는 식으로 만들어졌을 것입니다. 이게 바로 거대한 앱에서는 피해야 하는 중앙 설정의 예시입니다. 이 상황에서 어떤 개발자는 이렇게 생각할 것입니다. “이 컴포넌트가 아직 필요한 건가? 다른 파일로 교체하긴 해야 하는데, 이 컴포넌트는 다른 팀이 만든 거라서 내가 바꿔도 되는지 잘 모르겠네. 그냥 내일 작업해야겠다.” 이렇게, 중앙 설정 파일은 항상 무언가가 새로 추가되기만 하게 됩니다.

나쁜 예 2: webpack.config.js

다른 안좋은 예는 전체 앱을 빌드하는 데 필요한 모든 설정이 들어가있는 webpack.config.js 파일인데요. 개발 초기에는 이래도 괜찮지만, 언젠가부터 다른 팀에서 앱의 어디를 어떻게 고쳤는지를 다 알아야 하는 상황이 오게 됩니다. 앱 규모가 커질수록 알기가 어렵죠. 따라서 빌드 과정의 설정을 탈중앙화하기 위한 패턴이 필요합니다.

좋은 예: package.json

npm에서 사용하는 package.json이 탈중앙화의 좋은 예입니다. 각 패키지에는 “난 이런 패키지들에 의존하고, 실행 방법은 이거고, 빌드 방법은 이거야”라는 게 정의되어 있습니다. 모든 npm 패키지에 대한 설정을 파일 하나에 모아놓는 건 불가능합니다. 연관된 파일이 수천 수만 개에 달하기 때문입니다.

npm은 당신의 앱과 비교하기에 너무 거대한 예시일 순 있겠지만, 모든 앱은 어느정도 이상 커지면 중앙 설정의 문제점을 생각해야 하고, package.json과 비슷한 패턴을 적용할 필요가 있습니다. 모든 종류의 중앙 설정에 대해 제가 해결책을 가진 건 아니지만, ‘자바스크립트 안에 작성하는 CSS’의 아이디어가 여러 곳에 적용될 수 있을 것이라고 생각합니다.

이 문제를 조금 더 추상화해보면 앱의 의존성 트리의 모양을 결정하기 라고 부를 수 있겠습니다. 여기서 “의존성”은 넓은 의미로 보시면 되는데, 모듈 의존성 / 데이터 의존성 / 서비스 의존성 등 여러가지 의존성을 뜻할 수 있습니다.

enhance 개념을 이용한 의존성 관리.

img
라우터와 루트 컴포넌트 3개가 있는 의존성 트리의 예시. 라우터가 루트 컴포넌트들을 import한다.

우리의 실제 앱들은 엄청나게 복잡하지만 제 예시는 아주 단순합니다. 이 예시는 컴포넌트가 4개뿐입니다. 한 경로에서 다른 경로로 어떻게 움직일지에 대한 정보가 모여있는 라우터 하나와, 루트 컴포넌트 A, B, C입니다.

앞서 말했듯 이 예시 앱은 중앙집중적 import 문제를 가질 수 있습니다. 라우터가 모든 루트 컴포넌트들을 import하기 때문에, 루트 컴포넌트 하나를 지우려면 라우터에 가야 하고, 거기서 import문을 지워야 하고, 경로 정의도 지워야 하고, 결국 “2017 특별 휴일 문제”를 겪게 됩니다.

구글에서는 이 문제의 해결책으로 지금까지 한번도 공개된 적 없는 컨셉을 만들었습니다. enhance라는 건데요. enhance는 import 대신 사용하여 역 의존성을 나타낼 수 있습니다. 한 컴포넌트가 모듈을 enhance하면, 그건 그 모듈이 컴포넌트에 대한 의존성이 있음을 뜻합니다.

img
루트 컴포넌트들이 라우터를 enhance한다.

위 그림을 보시면 다른 건 같지만 화살표 방향이 거꾸로입니다. 라우터가 루트 컴포넌트를 import하는 대신, 루트 컴포넌트들이 enhance를 이용하여 자신의 존재를 라우터에게 알립니다. 즉 루트 컴포넌트 파일 하나만 지우면 관련된 정보를 다 지울 수 있다는 뜻입니다.

‘사람 문제’만 아니었다면 enhance는 아주 멋진 방법이었을 것입니다. 사람들은 이제 이런 걸 생각해야 합니다. “import와 enhance 중 뭘 써야 하지? 어떤 조건에서 무엇을 쓰는 게 좋을까?” 이건 사람 문제에서 특히 안좋은 케이스인데, enhance라는 개념이 무척 강력한 만큼 잘못 쓰이면 너무 위험하기 때문입니다.

따라서 구글에서는 오직 ‘생성된 코드’를 제외하고는 누구도 enhance를 하지 못하도록 정했습니다. 실제로 enhance를 사용하는 것은 생성된 코드와 아주 잘 어울리기도 하고, 생성된 코드의 본질적 문제 중 몇 가지를 해결해주기도 합니다. 생성된 코드는 눈에 보이지 않기 때문에, 이름을 추측해서 import해야 할 때가 가끔 생깁니다. 하지만 생성된 파일이 그저 뒤편에 있으면서 뭔가 필요할 때마다 enhance를 한다면 이런 문제는 사라집니다. 당신은 생성된 파일들에 대해 알 필요가 전혀 없어지고, 그 파일들은 모두 마법처럼 중앙 저장소(central registry)로 enhance합니다.

좀 더 구체적인 예시를 들어보죠. 파일 컴포넌트 하나가 있습니다. 여기에 코드 생성기를 돌려서 경로 정의가 담긴 작은 파일을 추출합니다. 그 경로 파일은 라우터에게 “나 여기 있어. import 해줘”라고 말합니다. 이 패턴은 여러 군데서 쓰일 수 있습니다. GraphQL을 사용하는 분은 라우터가 데이터 의존성에 대해 알고 있어야 할텐데, 이 때도 enhance와 비슷한 패턴을 사용할 수 있습니다.

(역자 주: 사실 저는 이 예제까지 봐도 enhance를 구체적으로 어떻게 사용한다는 것인지 정확히 이해하지 못했습니다. 원문의 댓글에서 많은 사람들이 더 자세한 예시를 달라고 하는 걸 보면 저만 그런 건 아닌 듯합니다. Malte는 한 댓글에서, enhance는 복잡한 어떤 개념이 아니고 import와 비슷한 수준의 추상화이며, regstry pattern을 생각하면 된다고 말했습니다.)

기본 번들의 크기가 커지지 않도록 조심하라.

제가 2017 특별 휴일 문제 다음으로 좋아하는 문제는 “쓰레기가 쌓인 기본 번들” 문제입니다. 기본 번들이란 사용자가 앱을 어떻게 사용하든 간에 언제나 로드되는 번들을 뜻합니다. 기본 번들이 특히 중요한 이유는 기본 번들이 커지면 그에 의존하는 모든 번들이 커질 것이기 때문입니다.

제가 구글 플러스 앱의 자바스크립트 인프라 팀에 들어갔을 때 앱의 기본 번들 크기는 800KB였습니다. 그러니까 당신이 구글 플러스보다 더 성공하는 앱을 만들고 싶다면, 적어도 800KB의 자바스크립트로 된 기본 번들은 가지지 않도록 하세요. 물론 안타깝게도, 그런 안좋은 상태가 되기란 매우 쉽습니다.

img
3가지 다른 의존성을 가진 기본 번들.

예시를 보죠. 기본 번들은 경로에 대해 알고 있어야 합니다. A 경로에서 B 경로로 가려면 이미 B에 대해 알고 있어야 하니까요. 그러나 기본 번들에 UI에 관련된 코드는 절대 포함되지 않아야 하는데, 사용자가 어떤 방법으로 앱에 접속하느냐에 따라 다른 UI를 보여줄 것이기 때문입니다. 즉 날짜 선택 폼이나 결제 플로우 같은 것들을 기본 번들에서 빼내야 하는 거죠.

그렇지만 이걸 어떻게 보장할 수 있을까요? import는 이런 점에서 아주 취약합니다. 당신이 무작위 숫자를 생성하는 기능이 필요해서 util 이라는 패키지를 기본 번들에서 import했다고 칩시다. 그러면 누군가가 “자동 주행하는 utility 기능이 필요한데?”라고 하는 순간 기본 번들에 자동 주행을 위한 기계학습 알고리즘이 포함되는 것입니다. import는 전이되기 때문에 (역자 주: A가 B를 import하고 B가 C를 import하면 A가 C에도 의존하게 됩니다.) 이런 일은 쉽게 일어날 수 있고, 시간이 지남에 따라 기본 번들이 커지게 됩니다.

구글에서 찾은 이 문제의 해결책은 의존성 금지 테스트 입니다. 이 테스트는, 예를 들어 기본 번들이 아무런 UI에도 의존하지 않도록 보장하기 위한 테스트입니다. React로 따지면, React의 모든 컴포넌트는 React.Component 를 상속해야 합니다. 따라서 React 앱의 기본 번들에 UI가 포함되지 않기를 원한다면 기본 번들이 React.Component 에 의존하지 않음을 보장하는 테스트를 추가하면 됩니다.

img
금지된 의존성들은 배제되었다.

앞의 예시를 다시 들어보면, 이제 누군가가 날짜 선택 폼을 추가하려고 할 때 테스트가 실패할 것입니다. 그리고 이런 테스트 실패는 고치기 쉬운데, 사람들이 정말 그 의존성을 기본 번들에 추가하려고 했다기보다는 import의 전이 과정에서 기본 번들이 날짜 선택 폼에 의존하게 되는 경우가 많기 때문입니다. 이러한 테스트 없이 2년 정도가 지났다고 생각해 보세요. 그때 가서 기본 번들의 의존성 문제를 해결하기는 엄청나게 어려워질 것입니다.

마치며.

1. 개발자가 옳은 방향으로 가도록 보장하는 테스트를 추가하라.

당신은 팀의 개발자가 어떤 일을 하든 간에, 가장 직관적인 방법이 옳은 방법이 되는 상태가 되기를 원할 것입니다. 그들이 길을 잃지 않고 자연스럽게 옳은 행동을 할 수 있도록요.

현재 그런 상태가 아니라면, 그를 보장하는 테스트를 추가하세요. 많은 분들이 놓치는 것인데, 당신의 앱 인프라에서 주요하게 지켜져야 하는 부분(major invariants of infrastructure)을 보장하는 테스트를 추가하는 걸 주저하지 마세요. 테스트는 수학 함수가 제대로 동작하는지만을 위한 것이 아닙니다. 앱의 인프라나 구조에 대해서도 테스트해야 합니다.

2. 인간이 판단해야 할 상황을 되도록이면 피하라.

앱을 구축하려면 비즈니스에 대해 이해할 필요가 있지만, 모든 개발자가 그럴 순 없습니다. 코드 분할도 마찬가지죠. 모든 개발자가 코드 분할이 어떻게 동작하는지 이해할 필요는 없습니다. 이런 것들 — 비즈니스 로직이나 코드 분할과 같은 것들을 도입할 때, 모든 개발자가 이해하지 않아도 그들이 잘 작업할 수 있도록 하는 방식으로 도입하도록 노력하세요.

3. 코드를 삭제하기 쉬운 구조로 만들어라.

코드를 지우기 쉽게 만드는 건 정말 중요합니다. 이 발표가 “아주 거대한 자바스크립트 앱을 구축”하는 것에 대해서인데요. 제가 해드릴 수 있는 최고의 조언은, “애초에 앱이 아주 거대해지지 않도록 하라”입니다. 그러기 위한 가장 좋은 방법은 너무 늦기 전에 코드를 지워나가는 것입니다.

4. 공감과 경험을 통해 올바른 추상화를 도입하기 위해 노력하라.

“잘못된 추상화를 도입할바에는 추상화를 하지 말라”는 말이 있는데요. 저는 이 말을 오해하는 분들을 종종 봅니다. 이 말이 뜻하는 바는 잘못된 추상화가 아주 커다란 비용을 야기하기 때문에 무척 조심해야 한다는 것입니다. 정말로 추상화를 하지 말라는 게 아닙니다. 아주 신중하게 하라는 뜻이죠.

발표 초반에 이야기했듯, 좋은 추상화로 가기 위한 방법은 팀의 개발자들에게 공감하고, 함께 생각하는 것입니다. 그들이 API와 추상화를 어떤 식으로 사용하게 될 것인지에 대해서요. 경험이 쌓이면 공감능력에 살을 붙여나갈 수 있습니다. 공감과 경험을 통해 당신은 당신의 앱에 적절한 추상화를 찾아낼 수 있을 것입니다.

이 발표가 흥미로웠다면 문서화에 대한 제 글도 읽어보시길 권해드립니다. 감사합니다.