[번역] Angular Universal
Angular Universal (Angular 2 Server Rendering)
- AngularU Server Rendering Presentation (6/25/2015): https://www.youtube.com/watch?v=0wvZ7gakqV4
- AngularConnect Full Stack Angular 2 Presentation (10/20/2015): https://www.youtube.com/watch?v=MtoHFDfi8FM
문서 정보
GitHub
원문 링크
문서 저자
- Jeff Whelpley (jeffwhelpley@gmail.com)
- PatrickJS (patrick@gdi2290.com)
- Tobias Bosch (tbosch@google.com)
- Jeff Cross (crossj@google.com)
참고사항
- 본 문서는 2014년 최초 작성되기 시작한 것으로 보이며 이후 저자들에 의해 얼마나 업데이트되어 왔는지, 혹은 베타 단계인 지금도 잘 관리되고 있는 것인지 확인이 어렵습니다. 하여 일부 내용이 현재 시점 (2016년) 기준으로 낡은 부분이 존재할 것입니다. (특히 선행 기술 부분) 다만 Angular 2 는 아직 베타 단계에 머물러있으므로 (2016년 3월) Angular 2 의 서버 렌더링을 설명하기 위한 기술 설계 파트 이후의 내용은 베타가 공개됐으나 아직 개발 진행 중인 Angular 2 의 기술 배경을 간단히 파악하기에는 무리가 없을 것으로 보입니다.
- 오역 및 어색한 해석이 다수 있을 수 있으니 이를 항상 염두에 두시길 바라며, 어색한 문장은 원문과 비교를 통해 꼭 제대로 짚고 넘어가기를 권해드립니다. 잘못된 해석, 어색한 해석등에 대한 지적을 환영합니다.
목차
- 목표
- 선행 기술
- 용도
- 주요 고려사항
- 추가 고려사항
- 기술 설계
- 향후 과제
- 참고 사항
목표 (Objective)
Angular 2 의 새로운 렌더링 기법은 앵귤러의 구동을 어플리케이션 계층과 렌더링 계층으로 분리시킨다. 어플리케이션 계층은 어플리케이션의 코드들에 직접 반응하는 API 와 동작들이 포함되며, 렌더링 계층은 UI 업데이트 수행를 위한 공통 프로토콜을 제공한다. 이러한 분리는 개발자들에게 동일한 추상 세트를 제공함으로 Angular 2 를 사용하는 어플리케이션들이 각기 다른 환경 / 플랫폼에서 실행되는 것을 가능케 해준다. 기본적인 렌더링 환경은 브라우저지만 웹 워커(web worker), 웹 서버 혹은 이외의 기기들을 포함한 환경을 포함할 수 있다. 이 문서는 웹 서버를 통한 Angular 2 의 화면 렌더링 뿐 아니라 서버를 통해 렌더링 된 화면을 브라우저에서 구동되는 Angular 2 클라이언트 어플리케이션 측에 매끄럽게 전달하기 위한 일련 과정의 솔루션 설계가 목적이다.
선행 기술 (Prior Art)
동형 자바스크립트(Isomorphic JavaScript) 라는 용어를 유행시킨 AirBnb 의 Spike Brehm 에 따르면 사람들이 이 용어를 사용하는 의미가 나뉘어지고 있으며, 크게 분류하면 단지 화면 렌더링 측면에서 공통 코드를 참조하는 것과 클라이언트와 서버 양측면 모두에서 코드를 공유하는 것으로 나뉘어진다고 한다. 또한 최근 Michael Jackson 은 이를 범용 자바 스크립트(Universal JavaScript) 라는 용어로 대체했다. 어떤 레벨에서건 동형 / 범용 화면 렌더링을 지원하는 기존 프레임워크들을 열거하면 아래와 같다.
Rendr
개요
- 배경 – Backbone 의 화면을 서버에서 렌더링하기 위해 AirBnb 의 Spike Brehm 에 의해 개발됨
- 인지도 – 3800 Guthub Stars. 단 AirBnb 는 Rendr 에서 React 로 갈아탐
- 동작 원리 – Rendr 의 기본 전략은 Backbone.view 대신 Rendr.view 를 사용하거나, Backbone.collection 대신 Rendr.collection 를 사용하는 것과 같이 Backbone 내 모든 코어 객체들의 래퍼들을 만들어 사용하는 것이다. 또한 Rendr 어플리케이션을 서버에서 구동하는 것에 특화된 라이브러리들이 존재한다.
세부 사항
- 템플릿 - handlebars 를 활용한 문자열 기반 (string-based) 의 템플릿 엔진을 사용함. 이런 유형의 템플릿 엔진들은 클라이언트나 서버 상의 템플릿 컨텐츠를 분석할 필요가 없어 동형 렌더링에 매우 적합하다. 또한 문자열 기반 템플릿은 렌더링 속도가 대체적으로 매우 빠르다.
- 라우팅 - Rendr.Router 는 서버 측의 Express 라우터와 클라이언트 측의 Backbone.Router 를 반영한다.
- 상태 전송 - 서버는 렌더링된 화면 내에 직렬화된 상태들을 저장하고, 클라이언트는 저장된 서버 렌더링 화면을 수신하여 직렬화된 상태들을 사용한다.
특이 사항
- 렌더링된 화면을 받은 뒤 babckbone subviews 와 jQuery 플러그인들이 어플리케이션의 상태를 수정함으로 리-렌더링 (re-rendering) 을 방지하여, 그로인한 새로 추가된 상태의 상실이나 성능 저하를 예방한다.
- 서버 측면에서 Express 와 궁합이 좋다.
Derby
개요
- 배경 - 2011년 Lever 의 CTO Nate Smith 에 의해 개발됨
- 인지도 - 3750 GitHub Stars. (2년 전엔 약 1600)
- 동작 원리 - Derby 는 Full-Stack 자바 스크립트 프레임워크다. 본 문서에 나열된 다른 프레임워크들이 서버 측 기능이 가미된 클라이언트 프레임워크의 느낌이 강하다면, 더비는 좀 더 서버 측에 포커스가 맞춰져 있다. Express 를 기반으로 하고 있으며 node.js 의 기본 라이브러리들을 사용한다.
세부 사항
- 템플릿 - 기본 옵션으로 클라이언트와 서버 양측에서 렌더링 가능한 반응형 템플릿을 사용함
- 라우팅 - 더비에 정의된 라우터는 서버 측의 Express 라우터에 반영되고, Express 라우터에 기반한 클라이언트의 커스텀 라우터에 반영된다.
- 상태 전송 - 서버가 HTML 을 제작하여 클라이언트로 내려보내고, 클라이언트는 서버 렌더링된 화면에 “연결” 된다. 이는 클라이언트 화면의 생성이 새로운 DOM 을 삽입하는 것이 아닌 일반적인 과정을 수행함을 의미하며, 서버 렌더링된 DOM의 존재 여부를 체크하여 이미 존재한다면 이를 갱신 (update) 한다.
특이 사항
- Express 와 궁합이 좋다.
Meteor
개요
- 배경 - 스타트업 벤쳐 회사로부터 개발 관리 되고 있음
- 인지도 - GitHub 에서 7번째로 각광받는 자바 스크립트 프로젝트이며, 인기가 갈수록 증가되고 있음. GitHub 26000 Stars. (불과 2년전엔 6000 이었음)
- 동작 원리 - 메테오는 모든 DB 를 지원하며 (database-everywhere), 데이터 집약적이고 (data-on-the-wire), 매우 심플한 순수 자바 스크립트 웹 프레임워크다.메테오의 주요 관심사는 실시간 웹과 모바일 어플리케이션이다.
세부 사항
- 템플릿 - 기본 템플릿 엔진으로 Blaze 를 차용한다. 클라이언트 측에서는 Blaze 를 사용한 기본 엔진만 동작하지만, 서버측 렌더링을 위해 사용자들이 Blaze 를 개조한 커스텀 엔진(meteor-ssr)이 존재한다.
- 라우팅 - 메테오는 기본적으로 라우팅을 지원하지 않지만, 역시 사용자들이 자발적으로 개발한 Meteor Iron Router 와 같은 라우팅 프로젝트들이 몇 몇 존재한다.
- 상태 전송 - 메테오는 기본적으로 서버 렌더링을 지원하지 않는다. 따라서 서버 렌더링 화면과 클라이언트간의 상태 전송 기능이 존재하지 않는다.
특이 사항
- 메테오를 활용한 서버 렌더링 기법들이 속속들이 등장하고 있지만, 메테오는 공식적으로 “네트워크를 통해 HTML 을 전송하지 않는다.” 고 명시하고 있다. (클라이언트 - 서버간 통신을 위해 HTTP 대신 DDP 를 사용한다.)
React
개요
- 배경 - 페이스북에 의해 제작되고 지난해 (주:2011년 페이스북 뉴스피드에 적용 2012년 인스타그램 적용, 2013년 오픈소스 공개) 공개되었음
- 인지도 - 공개된지 일년 남짓이 지났음에도 불구하고 GitHub 에서 8번째로 각광받는 자바 스크립트 프로젝트. (24000 stars)
- 동작 원리 - 리엑트는 오로지 화면 계층에만 관심을 맞추고 있으며, 고성능을 위해 가상 돔 (Virtual DOM) 을 비교하는 개념을 사용한다. 초기 렌더링부터 이후까지 전체 페이지가 가상 DOM 내에서 리-렌더링 되며, 가상 DOM 내에서 변경된 특정 부분들만 실제 DOM 에 적용된다.
세부 사항
- 템플릿 - React 는 하위 단계에서 보자면 템플릿 엔진이 존재하지 않는다. React 는 자바 스크립트 코드와 HTML 형식이 뒤섞인 JSX 라는 하위 단계의 자바 스크립트 코드로 가상 DOM 을 생성하는데, 이 위에 추상 계층이 존재한다. 클라이언트와 서버 각 렌더링의 유일한 차이점은 클라이언트 측에서 가상 DOM 이 변경사항을 실제 DOM 에 적용하는동안 서버 측에서는 가상 DOM 이 클라이언트로 전송할 HTML 문자열이 지속적으로 생성된다.
- 라우팅 - React 는 화면 계층 외엔 라우팅을 포함해 그 어떤 것도 신경쓰지 않는다. 단, 커뮤니티를 통해 공개된 react-router 가 클라이언트와 서버 라우팅을 위해 매우 대중적으로 사용되고 있다.
- 상태 전송 - React 는 클라이언트 측에서 초기 가상 DOM 을 서버 렌더링된 HTML 과 비교하기 때문에 서버 렌더링 화면에서 특이 상태를 지속적으로 전송할 필요가 없다. 즉 서버에서 렌더링된 HTML 이 곧 상태다. 이것에 유념해야 할 것은 클라이언트가 API 를 통해 데이터를 갱신한다는 것이다.
특이 사항
- 이같은 서버 렌더링 솔루션은 상대적으로 느린 편인데, 이는 서버가 HTML 을 연결시키기 위해 많은 부하가 걸리기 때문이다. 대부분의 템플릿 엔진들은 템플릿을 사전 컴파일하고 일부는 캐싱을 사용하여 첫 렌더링을 수행하지만 React 는 이같은 서버 측 최적화를 사용하지 않는다. 이같은 문제점을 보완하기 위한 작업이 진행 중에 있으며 다음 링크들을 통해 더 많은 정보를 얻을 수 있다,
- https://github.com/facebook/react/issues/1739
- https://github.com/facebook/react/issues/3009
- https://discuss.reactjs.org/t/whats-the-best-way-to-unblock-the-node-event-loop-due-to-slow-react-server-side-render/1817/1
- https://discuss.reactjs.org/t/react-dom-stream-a-streaming-server-side-rendering-library-for-react-as-much-as-47-faster-than-rendertostring/2286
- https://github.com/facebook/react/issues/3009#issuecomment-132001925
Ember FastBoot
Ember 측이 FastBoot 을 공개하긴 했지만, 실제로는 다방면 (in the wild) 에서 사용되고 있다. React 와 흡사하게 가상 DOM 을 사용하는 Glimmer 엔진 이란 또다른 서버 렌더링 솔루션을 사용해 개발이 진행중에 있는데(가상 DOM 은 상태 전송을 위해 비교를 사용한다), 템플릿 사전 컴파일 등과 같은 개선점이 추가될 예정이다.
개요
- 배경 - Tilde 의 Tom Dale 과 Yehuda Katz 에 의해 만들어짐
- 인지도 - 13173 GitHub Stars. 상대적으로는 적어보이지만, 매우 열정적인 사용자층을 가지고 있다.
- 동작 원리 - Ember 는 꾸준히 사용되어온 프레임워크지만, FastBoot 은 이제 막 개발되어 공개됐다. FastBoot 의 기본 전략은 서버 측에서 simple-dom 라이브러리를 사용하는 것이다.
세부 사항
- 템플릿 - Ember 는 이전까지 Handlebars 를 사용했지만 최근들어 신규 라이브러리인 HTMLBars 를 사용하고 있다. 어느것을 사용하건 이들은 일종의 실존 DOM 에 의존성을 가지고 있으며, 그렇기에 simple-dom 을 사용해야 한다.
- 라우팅 - Ember 라우터는 클라이언트와 서버 양측 모두에서 동작한다.
- 상태 전송 - 서버측에서 HTML 로 상태가 직렬화되며, 이후 브라우저의 어플리케이션이 이를 수령한다.
특이 사항
- 공개된지 불과 몇달 밖에 지나지 않았다.
Angular 2
다음과 같은 Angular 2 특징들이 공개되어 있으며, 서버 렌더링을 지원할 것이다.
용도 (Use cases)
다음의 각 용도들은 왜 서버사이드 렌더링이 당신의 클라이언트 웹 앱에게 중요한가에 대한 대답이 될 것이다. 본 섹션은 우리가 가장 중요하다 여기는 순서대로 용도들을 나열했다.
1. 체감 로딩 시간 (Perceived load time)
클라이언트 측 웹 어플리케이션은 초기화 시간이 대체적으로 느리다. 필라멘트 그룹이 최근 배포한 연구에 따르면 간단한 Angular 1.3 어플리케이션이 모바일 기기에서 초기화되는데 대략 3~4초 가량이 소요된다고 한다. 어플리케이션이 복잡하다면 결과는 더욱 나빠질 것이다. 이는 대부분 사용자 (소비자) 들이 어플리케이션을 모바일 기기를 통해 접근하는 것을 고려하면 큰 문제점이며, 그 외 환경에서 역시 문제가 될 수 있다. 이 사례 측면에서의 서버사이드 렌더링의 목표는 유저가 체감하는 페이지 초기화 성능을 낮추는 것으로, 사용하는 기기나 네트워크 상태와 관계 없이 사용자에게 페이지의 실 컨텐츠를 보여주기까지 1초 미만의 시간이 걸리도록 하는 것이다. 이 목표는 클라이언트 렌더링보다 서버 렌더링을 사용하는 것이 훨씬 더 달성하기 용이하다.
2. 실제 로딩 시간 (Actual load time)
클라이언트 웹 앱의 초기화 속도는 대체적으로 클라이언트의 코드 파일이 작을수록, 초기 화면의 필요한 화면만 렌더링 하는 것이 좌우하며, 이후 어플리케이션의 다른 부분들이 필요한 시점에 각자 렌더링되거나 백그라운드에서 렌더링한다. 이런 솔루션들은 대부분 빌드 시점에 중점을 두고 있지만, 서버에서 코드를 실행하는 방법을 통한 경험 누적으로 런타임 시점에서의 추가적인 최적화 방안을 얻을 수 있을 것이다.
3. 클라이언트 측 성능 (Client side performance)
일반적인 서버 렌더링은 페이지의 초기 로드가 포함되고 이후부터 클라이언트가 렌더링을 수행한다. 클라이언트가 어플리케이션의 제어권을 가지고 있더라도 서버로부터 완전히 렌더링된 조각들을 받아오는 것이 클라이언트가 스스로 렌더링 하는 것보다 더 빠른 경우들이 있다. 예를들면 매우 정적이지만 복잡한 상품의 페이지같은 경우로, 다음의 상황들을 살펴보자.
- 페이지의 어떤 부분이 매우 느린 API 로부터 데이터를 받음으로 인해 멋드러진 최적화가 전혀 효과를 보지 못하고 렌더링에 오랜 시간이 걸린다. 해당 데이터는 그리 자주 바뀌는 데이터가 아니기에 완전히 렌더링된 부분을 서버측에 캐싱하여 가지고 있고, 클라이언트가 해당 부분을 채울 데이터를 매번 끌어오는 것이 아니라 서버에 캐싱되어 있는 온전히 렌더링 된 부분을 끌어다 쓸 수 있다.
- 위와 같이 API 호출을 배제하는 것은 그리 나쁜 방법이 아니다. 이는 서버의 런타임 렌더링이 이점을 가지고 있다 하더라도 논쟁의 여지가 있지만, 부분에 따라 어떤 부분을 비동기적으로 끌어와 클라이언트에 캐싱하거나 상황에 따라선 해당 부분을 빌드 시점에서 렌더링하는 것 또한 고려할 수 있다.
- 특정 시각화를 위해선 클라이언트보다 서버측에서 렌더링 하는 것이 더 효과적일 수 있다. 최종 결과는 비교적 작지만, 방대한 데이터 기반의 무거운 계산이 필요한 복잡한 그래프나 차트에 있어 특히 효과적이다.
트위터가 이런 방식의 최적화를 지난 2년간 진행해왔음을 주목해야 한다. 이 방식은 특히 저전력 모바일 기기 환경에서 이점이 명확하게 나타난다.
4. 검색 엔진 최적화 (SEO)
구글 검색 수집기의 클라이언트 렌더링 컨텐츠 인덱싱 능력이 갈수록 좋아지고 있기는 하지만 여전히 난관이 존재한다.
- 우선 수집기는 완벽하지 않다. (적어도 현재까지는) 수집기가 렌더링된 정보를 인덱싱하지 못하는 몇가지 상황들이 존재하며, 이는 자바 스크립트 태생적 한계들 혹은 비동기 로딩 시간으로 인해 발생한다.
- 서버 렌더링을 활용하면 검색기가 페이지의 컨텐츠가 유저에게 전달되기까지 걸리는 시간을 정확히 감지할 수 있으며 (문서 로딩 완료), 이는 클라이언트 렌더링으로는 제공하기 어려운 과제다. (이전의 사례에서 언급했듯이, 감지가 되더라도 대부분 서버 렌더링보다 느리다)
- 클라이언트에서 렌더링을 도맡는 웹 어플리케이션이 서버 렌더링을 사용하는 웹 사이트에 비해 검색 노출에 있어 경쟁력이 높았던 사례가 현재까지 없다. (평면 TV 나 2015 베스트 세단 과 같은 주요 구매품을 떠올려 보라)
미래에는 서버 렌더링이 SEO 를 위한 고려 사항이 아닐 것이다. 다만 오늘날 소비자들은 검색 순위에 매우 민감하기에 서버 렌더링이 필요하다.
5. 브라우저 지원 (Browser Support)
웹 컴포넌트와 같은 고도화된 웹 기술들을 사용하는 것에 있어 난점은 구형 브라우저들의 지원을 유지하는 것이며, Angular 2 가 에버그린 브라우저 (주:자동 업데이트 정책의 브라우저) 들만을 대상으로 하는 이유다. 다만 어플리케이션 개발 사례들에 따르면 어플리케이션 개발자들이 최신 웹 플랫폼의 이점을 차용하고 있음에도 리치-클라이언트 사용자 (주:Fat Client. 전통적 PC사용자들을 의미하는듯?)나 서버 사용자들을 위해 구형 브라우저를 지원해야 할 경우가 발생한다. 예를 들면 다음과 같다.
- 정보를 제공하는 것이 주 역할이며, 구형 브라우저 사용자들에게도 클라이언트 측의 특별한 기능 없이 정보가 노출되기만 하면 문제가 없는 경우가 있다. 이 경우엔 에버그린 브라우저 사용자들에겐 클라이언트 기반 어플리케이션을 제공하더라도 구형 브라우저 사용자들에게는 온전히 서버에서 렌더링된 싸이트 형태만 제공되도 문제가 없다.
- 어플리케이션이 IE9 를 반드시 지원해야 하는 경우. (에버그린 브라우저는 아니지만 IE8 같이 나쁘진 않다) 클라이언트 웹 어플리케이션의 기능들이 대부분 동작할 것이나, IE9 가 지원하지 않는 한가지 기능 요소가 있다. 이 한가지 기능 요소를 위해 어플리케이션은 서버로부터 완전히 렌더링된 부분 HTML 을 받아와 적용하는 기능이 적용되어야 한다.
이런 사례의 언급이 Angular 2 는 IE6 과 같은 구형들을 지원할 것이라는 의미는 아니며 어디까지나 개별 사례일 뿐이다. 어플리케이션은 모던 브라우저의 특징들이 요구될 수도 있고 서버 렌더링을 사용하지 않는 것이 도움이 될 수 있다. 이는 과거 브라우저들을 대상으로 정적 서버 렌더링으로 처리되는 컴포넌트나 페이지, 웹 싸이트 들이 수용되는 사례다.
6. 링크 미리보기 (Link Preview)
링크들의 싸이트 프리뷰 기능을 제공하는 프로그램들은 대게 서버 렌더링에 의존한다. 이는 링크 대상 페이지의 형상을 캡쳐해야 하는 복잡한 과정을 거쳐야 하기 때문으로, 한동안은 서버 렌더링에 계속 의존하게 될 것이다. 이런 케이스들의 적합한 소셜 플랫폼 예시는 페이스북, 구글 플러스, 링크드인 등을 꼽을 수 있다. 검색 엔진 최적화의 용도와 마찬가지로 이는 소비자에 직접 노출되는 어플리케이션의 주 관심사다.
주요 고려사항 (Technical Requirements - Primary)
이 섹션은 Angular 가 제공하는서버 렌더링 솔루션이 추구하는 주요 관심 사항들을 제시한다.
쉬운 동작 환경 (“It just works”)
Angular 는 마치 준비된 도구처럼 자바 스크립트 백엔드 서버를 사용하는 Angular 2 웹 어플리케이션을 손쉽게 렌더링 할 수 있어야 한다. 완벽히 렌더링 되어야 하는 것도 아니고 최적의 상태여야 하는 것도 아니지만 어쨌든 렌더링은 돼야 한다. 준비된 도구로써의 기능적 목표는 개발자들에게 서버 렌더링 솔루션의 기준을 제공해주는 것이다. Angular 2 어플리케이션이 DOM 을 직접 제어하지 않는 한 (창 혹은 문서 객체를 Angular 2 API 를 사용하는 것 외의 방법으로 접근하는), 서버에서 렌더링되는 요소들은 클라이언트에서 렌더링되는 것과 같은 방법으로 렌더링되어야 한다.
매끄러운 상태 전송 (Seamless state transfer)
클라이언트는 서버로부터 전달받은 레더링 된 화면을 매끄럽게 인계받아야 한다. 즉, 서버 렌더링된 페이지를 통해 데이터 전송과 사용자의 상호작용 (Interaction) 이 발생하는 순간 지그재그 형태의 데이터 흐름이 일어나선 안되며, (예를들어 사용자가 폼에 데이터를 입력하는 순간) 어떤 이유에서건 흐름이 방해받아서는 안된다. 그리고 만약 사용자가 검색창에 뭔가를 입력하고 있다면, 검색창의 문자들과 검색창에 포커스된 커서의 상태가 데이터가 전송된 후에도 페이지 상에 남아 있어야 한다.
성능 (Performance)
위의 용도 부분에서 언급했듯이, 개발자들이 서버 렌더링을 원하는 가장 중요한 이유는 성능 향상이다. 그러므로 Angular 가 마주한 각각의 성능 이슈 해결을 (마치 벤치 프레스를 측정하듯이) 위한 솔루션은 매우 중요하다. 다음의 조건들을 살펴보자.
- 최신 사양의 맥북에 탑재된 데스크탑 브라우저
- 응답속도 10ms 미만
- 서버 성능은 하나의 AWS t2 instance 와 동등하게
- 단일 요청 (Request)
- 11MBps 를 넘는 인터넷 전송 속도
위와 같은 조건 하에서 우리의 솔루션은 다음의 결과를 만족해야 한다.
- 서버 렌더링 시간 100ms 미만
- 체감 로딩 시간 1초 미만
- 실제 로딩 시간 3초 미만
추가 고려사항 (Technical Requirements - Seconds)
위의 주요 고려사항들이 충족된 후엔 다음의 사항들을 추가로 고려할 것이다.
확장성 (Extensiblility)
Angular 서버 렌더링 솔루션이 확장성을 가지기 위해선 크게 두가지를 충족시켜야 한다. 첫째로 서버 측 렌더링 엔진은 어떤 Node.js 어플리케이션과도 통합될 수 있어야 하며, 둘째로 비 자바스크립트 백엔드 영역 역시 적용이 가능해야 한다.
컴포넌트 라우팅 (Component Routing)
대게 라우팅은 어플리케이션을 어떻게 구현되었는가에 따라 달라지지만 (UI 라우터이거나 Angular 라우터 이거나), 서버 렌더링 솔루션은 세부 컴포넌트들을 라우팅하기 위해 일반적 방법이 필요하다. 이는 Angular 어플리케이션이 서버에게 부분적인 렌더링을 요청할 수 있도록 해줄 것이다.
최적화 (Optimizations)
성능의 병목 지점은 대게 존재하기 마련이고, 각각의 어플리케이션들은 각자 자신들만의 고유한 문제점들이 있다. Angular 서버 렌더링 솔루션은 다음의 옵션 기능들을 제공함으로 성능 최적화에 도움을 주고자 한다.
- 서버 / 클라이언트 - 개발자는 어플리케이션의 부분들이 서버에서 렌더링 될 것인지, 클라이언트에서 렌더링 될 것인지 혹은 양쪽 모두에서 렌더링 될 것인지 페이지 혹은 컴포넌트 레벨 단위로 지정할 수 있어야 한다.
- 서버 캐싱 - 페이지 레벨의 캐싱은 웹 서버에 의해 처리될 수 있지만, 우리는 어플리케이션 내부의 컴포넌트 단위로 캐싱을 지원하기를 원한다. 이는 설정 가능해야 하며, 어떤 매체도 백엔드에서 캐싱될 수 있어야 한다.
- 지연 로딩 (Lazy loading) - 서버 렌더링 솔루션은 어플리케이션의 실 로딩 시간을 줄이기 위해 지연 로딩의 기능이 통합되어야 한다.
- 익명 서버 렌더링 (Anonymous server rendering) - 경우에 따라선 익명의 사용자들에게 서버의 렌더링을 허용하는 것이 유익할 수 있으며 (사용자 정보(context) 를 제외한), 이런 페이지들은 보다 왕성한 캐싱이 이뤄진다. 이는 공개된 컨텐츠 제공이 주를 이루고 어플리케이션들에게 주로 사용되며, 추가된 아주 약간의 사용자 특화 컨텐츠 (우측 상단의 사용자 프로필 사진 같은) 들은 클라이언트 렌더링만으로 충분히 감당할 수 있다.
기술 설계
이 섹션은 Angular 를 사용한 서버 렌더링 구현을 위한 설계를 다룬다.
접근 (Approach)
상위 단계에서 보자면, Angular 2 서버 렌더링 솔루션은 크게 두가지 부분으로 나눌 수 있다.
- 서버에서 렌더링 수행
- 서버의 화면을 클라이언트 화면으로 이행
Angular 2 의 서버 렌더링 구조는 서버 돔 렌더러(ServerDomRenderer) 라는 (추후 서술할) 구성이 추가된 구조를 가지고 있다.
서버 화면을 클라이언트 화면으로 전송하기 위해 preboot 을 사용하며 이 역시 추후 서술한다.
전체 흐름 (Overall Flow)
Angular 의 렌더링 솔루션 흐름의 변화점을 간단히 살펴보자. 우선 기본적인 렌더링 흐름은 다음과 같다.
- HTTP GET 요청이 서버로 전송됨
- 서버가 다음의 사항들을 렌덜링
- 초기화 동작에서 유저가 보게 될 HTML 이 서버 돔 렌더러를 통해 렌더링 됨
- 함께 동작할 preboot 자바 스크립트 코드 (preboot 섹션 참조)
- 브라우저가 서버로부터 초기화 데이터를 받음
- 서버 화면이 사용자에게 노출
- preboot 이 이벤트들을 기록 시작
- 추가 외부 자원를 위한 요청이 생성됨 (이미지, JS, CSS 등…)
- 최초 외부 자원이 로딩된 후, Angular 클라이언트가 기동 (bootstrapping) 됨
- 클라이언트 화면 렌더링
- 기동 완료 후, preboot.done() 호출
- Angular 가 기동되기 전의 사용자 행태에 (텍스트박스 입력, 클릭 버튼 등) 따른 변화를 반영한 어플리케이션의 상태 (state) 를 조정하기 위해 preboot 가 기록된 이벤트들을 반복
모듈 - 서버 DOM 렌더러 (Module - ServerDomRenderer)
이 모듈은 렌더링 API(render api interface)를 수행하며 요구사항들은 이곳에서 정리되어 있다. 이 모듈은 브라우저의 DOM 과 연계되는 데이터 구조를 생성하고 유지할 것이다. 웹 서버로부터 전송되거나 테스트용으로 생성된 객체 내부의 toString() 메소드를 통해 HTML 문자열을 쉽게 생성할 수 있다.
(보다 자세한 사항들은 후술 예정)
모듈 - 서버 플러그인 (Module - Server Plugin)
이 모듈은 서버 측에서 Angular 어플리케이션을 실행하기 위한 핵심 라이브러리다. 기본적으로 Express 나 Hapi 와 같은 node.js 기반의 웹 서버 프레임워크와 쉽게 연계하기 위한 API 들에 덧붙여 커스텀 기동 도구로 구성될 것이다.
(보다 자세한 사항들은 후술 예정)
모듈 - Preboot (Module - Preboot)
서버 측에서 생성된 화면을 클라이언트로 전송하는 역할을 담당한다. Angular 의 문맥 밖에서 Angular 에 의존성 없이 사용 가능한 독립 모듈로써 어떤 프레임워크와도 조합이 가능하다. 페이지를 방문하는 사용자들을 위해 클라이언트가 수용해야 할 사항들은 천차만별이다. 하여 Preboot 는 개발자들이 여러 상황에 맞는 설정을 적용시킬 수 있도록 만들어졌다.
핵심 기능
- 이벤트의 기록과 재생 - 어떤 이벤트를 감지하고, 이벤트의 반복 동작을 위한 세부 옵션 제공
- 이벤트에 즉각 반응 - 때로는 이벤트 발생에 즉시 반응할 수 있어야 한다. (클라이언트가 준비되지 않았더라도)
- 리렌더링 페이지에서도 포커스 유지 - 텍스트박스에 포커스가 맞춰진 상태에서 페이지가 리렌더링되면 클라이언트 화면에서 일치하는 텍스트박스에 다시 포커스를 맞추도록 한다.
- 클라이언트 측 리렌더링 버퍼링 - Angular 2 는 화면 갱신이 일괄적으로 이뤄지지만 클라이언트 화면의 갱신을 위해 버퍼링이 필요한 경우가 발생할 수 있으며, 이를 통해 서버 화면에서 완전히 렌더링된 클라이언트 화면으로의 부드러운 전환이 가능하다. 이런 버퍼링 지원은 서버 화면에서 크라리언트 화면으로의 전환이 1 프레임 내에 이뤄지는 것을 보장할 것이다.
- 페이지 기동 중 클릭 발생시 기동 완료 때까지 상태 동결 - 개발자가 선택한 옵션에 따라 사용자가 페이지 내 버튼을 클릭할 경우 해당 이벤트의 처리를 완료할 때 까지 더 이상의 처리 요구 동작이 발생하지 않도록 페이지를 동결 시킬 수 있다.
설치
Preboot 는 클라이언트 코드를 생성하는 서버 사이드 라이브러리이며, npm 을 이용한 설치가 필요하다.
npm install preboot
설치 후 서버 측에서는 다음과 같은 코드를 추가해야 한다.
var preboot = require('preboot');
var prebootOptions = {}; // see options section below
var clientCode = preboot(prebootOptions);
그리고 서버 측의 템플릿 HEAD 부분에 클라이언트 코드를 삽입하자. 우리는 DOM 에 웹 어플리케이션 루트 (역자주 : 루트를 의미하는 Angular 2 의 커스텀 DOM 을 의미하는 듯> 가 존재하는 템플릿에서 Preboot 이 오직 한번만 기록을 시작하길 바라며, 이를 위해 좀 더 나은 방법을 고민 중이다. (참고 : onLoad 에서의 실행을 시도해봤으나 콜백 호출이 제 때 실행되지 못하는 문제가 있었다.) 해서 현 상황에선 웹 어플리케이션 서버사이드 템플릿의 웹 어플리케이션 루트 바로 다음 부분에서 ‘preboot.start()’ 코드를 실행한다.
<web-app-root-here>
</web-app-root-here>
<script>
preboot.start();
</script>
마지막으로 클라이언트 측 웹 어플리케이션이 완전히 준비되면 (alive) Preboot 에게 이벤트 반복을 진행해도 좋다고 알려야 한다.
preboot.complete();
옵션
Preboot 는 5 가지 형태의 옵션들을 지정할 수 있다. 다음의 옵션들과 더불어 향후에는 preset 컨셉이 추가할 계획이며, preset=angular 와 같은 선언이 되면 해당 세트에 맞춰진 옵션들이 적용될 것이다.
선택자들 (Selectors)
- appRoot - 화면의 루트 요소를 찾기 위해 제공되는 선택자 (defuault 는 <body>)
전략 (Strategies)
플래그 (Flags)
모든 플래그들은 기본적으로 false 상태를 가진다.
- focus - true 로 설정하면, 페이지가 리렌더링될 시 focus 를 추적하여 유지한다.
- buffer - true 로 설정하면, 페이지 기동 완료 후 보여질 부분을 숨김 상태로 미리 작성한다.
- keyPress - true 로 설정하면, 문자 입력부 (textbox, textarea) 내에서의 모든 키 입력이 서버 화면에서 클라이언트 화면으로 전송된다.
- buttonPress - true 로 설정하면, 버튼 클릭이 기록되고 기동이 완료될 때까지 UI 가 동결 (freeze) 된다.
- pauseOnTyping - true 로 설정하면, 문자 입력부에서 포커스가 해제될 때까지 Preboot 의 준비가 완료되지 않는다.
- doNotReplay - true 로 설정하면, 기록된 이벤트들이 반복되지 않는다.
이벤트 동작 흐름 (Workflow Events)
다음의 글로벌 이벤트들은 Preboot 의 동작 흐름에 영향을 미친다.
- pauseEvent - 기록된 이벤트들의 재생을 지연시킴 (디폴트는 ‘PrebootPause’)
- resumeEvent - 이벤트들의 재생을 다시 시작 (디폴트는 ‘PrebootResume’)
빌드 인자 (Build Params)
- uglify - 개발자가 직접 클라이언트 코드의 난독화 (uglify, 역자주 : 일반적으로 minify 로 통하는 것을 의미하는듯) 를 수행할 수도 있지만, 이 옵션을 통하면 Preboot 에게 작업을 맡길 수 있다.
감지 전략 (Listen Strategies)
감지 (listen) 옵션은 사전 정의 전략명을 지정할 경우 문자열 (string) 을 사용하고, 그 외 설정은 설정 객체 혹은 설정 객체의 배열등으로 지정할 수 있으며, 각 설정 객체들은 다음의 값들을 가질 수 있다.
- name - 다음의 사전 정의 전략 값들 중 하나를 지정해야 한다.
- selectors - 선택자를 이벤트 배열에 매핑하기 위해 객체를 사용
- attributes - 이 전략은 서버 화면의 요소들 중 특정한 속성명이 사용되고 있는지를 확인한다. (기본 감시 값은 “preboot-events”) 만약 와 같이 preboot-events 속성이 사용되고 있으면, 해당 input 요소에서 발생되는 모든 키 입력과 포커스 이벤트들은 추적된다.
- event_bindings - 서버 화면의 HTML 에서 선언된 Angular 2 의 이벤트 바인딩을 사용한다.
- getNodeEvents - 커스텀 전략의 구현체
- preventDefault - 이벤트의 전파를 방지
- dispatchEvent - 지정된 커스텀 이벤트가 발생시 디스패치되도록 설정
- trackFocus - 지정된 노드에 포커스 발생시 이를 추적
- doNotReplay - 지정된 이벤트들의 반복 방지
- attributeName - attributes 전략에서만 사용되는 옵션으로, 이벤트 기록을 명시할 속성명을 지정. (기본값은 ‘preboot-events’)
- eventsBySelector - selectors 전략에서만 사용되는 옵션으로, 문자열 선택자가 이벤트 배열에 매핑될 객체를 지정
- action - 이벤트가 발생했을 시 실행될 커스텀 펑션
각 전략은 eventName(발생한 이벤트명), node(이벤트가 발생한 DOM 노드) 의 두가지 값을 포함한 객체 배열을 반환한다.
반복 전략 (Replay Strategies)
반복 옵션은 사전 정의 전략명을 지정할 경우 문자열을 사용하고, 그 외 설정은 설정 객체 혹은 설정 객체 배열등으로 지정할 수 있다.
각 설정 객체는 다음 둘 중 하나를 포함해야 한다.
- name - 전략 명칭
- replayEvents - 커스텀 전략 구현체
사전 정의 전략이나 커스텀 구현체는 전달된 모든 이벤트들의 반복을 시도한다. 간혹 일부 이벤트들은 반복되지 않을 수가 있는데, 이런 이벤트들은 이벤트 배열로 반환된다. 만약 Preboot 의 반복 전략 사용이 명시되었다면, 지정된 전략은 배열의 이벤트들을 다룬다.
반복 전략의 사전 정의 전략들은 다음과 같다.
- hydrate - 이 전략은 서버 화면과 클라이언트 화면을 동일하게 취급한다. 달리 말하자면, 서버 화면을 위한 메모리의 DOM 요소들이 클라이언트 화면에도 남아있음을 의미한다. 하여 반복 이벤트가 발생하면 메모리에 존재하는 노드를 간편하게 사용하면 된다. 존나쉽군? (Easy peasy)
- renderer - 클라이언트 화면은 대게 리렌더링 작업 동안 서버 화면과 멀어진다. (역자주 : 달라진다.) 이는 메모리 상의 노드들이 더이상 쓸모가 없음을 의미한다. 하지만 이런 메모리의 노드들은 클라이언트의 다시 렌더링된 화면에서 포커싱을 맞추는 등의 반복 재생을 위한 노드 파악의 근거로 사용될 수 있다.
동결 전략 (Freeze Strategies)
유저에게 클라이언트 웹 어플리케이션의 구동이 완료되기 이전에도 기능성을 제공하기 위해서는 페이지에 이벤트가 발생하면 UI 를 동결시킬 필요가 있다. 예를 들어 사용자가 FORM 의 데이터를 모두 작성하고 SUBMIT 버튼을 클릭하면, 사용자는 클라이언트가 FORM 의 데이터를 모두 처리하기 전까지 어떤 일도 하지 않아야 한다. 대게 이런 작업은 화면을 dim 처리 (overlay) 하거나 로딩바를 노출 (spinner) 하는데, 동결 전략은 이런 작업들을 어떻게 커스터마이징하는가를 결정한다.
- 참고 : 개발자는 DIM 처리나 로딩바 노출을 본 라이브러리 내 dist 디렉토리의 CSS 파일을 재정의하여 커스터마이징 할 수도 있다.
향후 과제 (Future Items)
본 섹션은 상기 언급된 초기 솔루션들이 제공되기 전까지는 우선 순위에 밀린 계획들에 대해 언급한다.
비 자바 스크립트 서버 렌더링 지원 (Non-JS server rendering)
다른 컴포넌트들의 설계가 잘 마무리되면 비 자바 스크립트 서버를 지원할 예정이다. 비 자바 스크립트 서버 렌더링의 핵심은 웹 어플리케이션을 정적으로 분석하는 것과 Nashorn 과 같은 방법으로 Java 를 통해 JavaScript 를 실행하는 것 중 선탁해게 될 것이다.
패키징 서비스 (Packaging service)
메인 렌더링 솔루션의 설계가 마무리되면, 웹 어플리케이션의 실제 로딩 시간을 감소시키고 지연 로딩을 구현하기 위한 패키징 서비스 작업을 진행할 예정이다.
참고사항 (Notes)
본 섹션은 개발 회의을 통해 제시되었으나 문서상에 제대로 반영되지 않은 사항들을 기재한다. 이 섹션의 내용들은 점차 제거되어 문서상에 정식 반영될 것이다.
제한사항
- 비 자바 스크립트 서버 지원은 매우 어려운 사안이며, 우선 통용적인 솔루션을 만들도록 한다.
- 일일이 열거하기 어려운 요청 인자값들에 의한 결과를 가정해야 하기에 온라인 상태여야 한다.
- 사용자 입력을 포함한 여럿..
- 초기화 단계의 페이지 뿐 아니라 이후 어떤 화면이라도 사전 렌더링을 지원해야 한다.
- 컴포넌트의 지연 로딩 코드를 지원하여 사전 렌더링된 어플리케이션의 영역을 포괄해야 한다.
- 컴포넌트가 완전히 로드될 때까지 사용자 이벤트를 기록하고 반복하기 위해
- 어플리케이션의 네트워크 비용을 줄이기 위해
- 지연된 컴포넌트들의 기동을 지원해야 한다.
- 예를들어 메시지가 긴 쓰레드로 나뉘어진 경우 사용자의 클릭과 같은 지정 조건에 따라 컴포넌트를 기동시킬 수 있어야 한다.
- 지정 가능한 조건들
- 지정된 이벤트에 따른 기동
- 지정된 문구의 변화에 따른 기동
- 각 컴포넌트를 위한 코드가 로딩 되는 순간에 기동
기준 내역
- 요청들 (requests) 은 최대한 작게 -> 소스 파일들을 연계시키는
- 전송된 데이터들을 기반으로 기반으로 네트워크 시간과 발생 트래픽을 추정한다.
- Angular 2 내에 벤치마킹 기능을 삽입하여 참조할만한 지표들을 얻을 수 있도록 한다.
주요 발상
- 템플릿은 재사용하되, 어플리케이션의 로직은 서버에서 동작하지 않도록
- 로드된 컴포넌트는 라우터를 통해 로드되었건 사전 렌더링을 통해 로드되었건 동일한 입장을 가져야 한다.
- 지연 로딩 시 위해 라우터 로딩 시와 동일한 기법을 사용 (동적 컴포넌트)
- 컴포넌트에게 데이터를 전달 시 동일한 기법을 사용 (코드를 통한 데이터 바인딩이건, 서비스를 통한 데이터 바인딩이건)
영향성
- 비 자바스크립트 서버는 두개의 데이터 세트를 생성해야 한다.
- 하나의 세트는 클라이언트 기동을 위해 전송되며
- 하나의 세트는 화면에 레더링되어 표기되야 할 모든 항목들의 값이 정해진 규격으로 생성되어야 한다.