PRPL Pattern
우리가 만든 앱을 전 세계에 서빙하는것은 꽤나 도전적인 일이다. 이를 위해서는 성능이 좋지 않은 단말 혹은 네트워크 환경이 좋지 않은 곳에서도 일정 이상의 성능을 보장해야 한다. 이런 어려운 환경에서도 앱을 효율적으로 로드할 수 있도록 하기 위해 PRPL 패턴을 사용할 수 있다.
PRPL패턴은 성능과 관련된 아래의 주요 고려 사항들에 초점이 맞춰져 있다.
- 서버와의 라운드트립을 줄이고 로딩 타임을 감소시키기 위해. 클라이언트 측에 중요 리소스를 효율적으로 전달한다.
- UX를 위해 초기 탐색 경로에 대한 페이지를 가능한 빨리 렌더링한다.
- 백그라운드에서 에셋들을 캐시하여 서버로의 요청을 줄이고 오프라인 상태일때에 더 나은 경험을 제공한다.
- 자주 요청되지 않는 에셋들을 지연로딩한다.
어떤 사이트에 접속하려면 먼저 필요한 리소스들을 다운로드 받기 위한 요청을 해야 한다. 그 다음 서버로부터 앱의 초기 html을 포함하는 엔트리포인트 파일들이 응답된다. 브라우저의 html파서는 서버에서 이런 데이터들을 받는 즉시 처리하기 시작한다. 처리중에 파서가 스타일이나 스크립트와 같은 추가적인 리소스를 받아야 하는것을 알게 되면. 해당되는 리소스를 받기 위한 http요청을 추가적으로 보내게 된다.
이렇게 반복적으로 리소스를 요청하면 성능에 악영향을 줄 수 있다. 앞서 언급했던대로 서버와 클라이언트간의 “라운드트립 최소화”를 적용해 보도록 하자.
우린 오랫동안 서버와 클라이언트 간 통신을 위해 HTTP/1.1 프로토콜을 사용해 왔다. HTTP/1.1은 HTTP/1.0 에 비해 많은 향상이 이루어졌다. 예를 들어 get 요청에 keep-alive
헤더를 포함시켜 클라이언트 와 서버간 TCP커넥션을 유지시킬 수 있게 되었지만, 이 기능에는 해결되어야 하는 몇가지 문제들이 있다.
HTTP/2 는 HTTP/1.1 에 비해 몇몇 중요한 업데이트가 이루어졌는데. 이로 인해 서버와 클라이언트 간의 메시지 교환을 쉽게 최적화할 수 있게 되었다.
HTTP/1.1이 요청과 응답을 위해 개행으로 구분되는 일반 텍스트 프로토콜을 사용하는 반면, HTTP/2 는 요청과 응답들을 프레임이라 불리는 작은 조각들로 나눈다. 하나의 HTTP요청은 헤더와 본문을 포함하고 있으며 최소 2개의 프레임을 포함한다. 바로 헤더 프레임과 데이터 프레임이다.
HTTP/1.1은 클라이언트와 서버 간 TCP커넥션을 최대 6개까지만 만들 수 있다. 새로운 요청이 동일한 TCP커넥션을 통해 보내지려면. 이전 요청이 완료되어야 한다. 이전 요청이 완료되는데 오래 걸리면 클라이언트로부터 보내어진 다른 요청들은 모두 블록된다. 이 이슈를 head of line blocking이라 하고. 특정 리소스에 대한 로딩 타임을 증가시킬 수 있다.
HTTP/2는 단일 TCP커넥션 내에서 여러 양방향 스트림을 만들 수 있다. 클라이언트와 서버는 이 여러 스트림들 내에서 여러 요청과 응담 프레임들을 주고받게 된다.
서버가 특정 요청에 대한 모든 프레임을 받고 나면. 이를 재조립하고 응답 프레임을 생성한다. 이 응답 프레임은 클라이언트에 보내지며 똑같이 재조립된다. 이 스트림은 양방향으로 데이터 전달이 가능하기 때문에 하나의 스트림에서 요청, 응답 프레임 모두 전달이 가능하다.
HTTP/2는 단일 TCP커넥션에 여러 요청을 보낼 수 있어 위에 언급한 head of line blocking이슈가 발생하지 않는다.
HTTP/2 는 server push라는 최적화된 데이터 요청 기능이 제공된다. 리소스를 받기 위해 각각의 HTTP요청을 명시적으로 보내는 대신, 서버는 추가적인 리소스들을 의도적으로 “push”할 수 있다.
이렇게 서버로부터 “push” 된 리소스들은 클라이언트 도착과 동시에 브라우저 캐시에 저장된다. 엔트리 파일(eg. index.html)이 파싱되다가 추가 리소스가 발견되었을 때 이전처럼 별도의 HTTP요청을 보내는 대신 리소스를 캐시로부터 꺼내어 사용하게 된다.
리소스 push가 추가적인 리소스를 받는데 소요되는 시간을 줄여주긴 하지만, 해당 리소스들은 HTTP캐시 처리되지는 않는다. push된 리소스들은 다음 번 사이트 방문에는 사용할 수 없기 때문에 다시 요청을 하게 될 것이다. 이런 문제를 해결하기 위해 PRPL패턴에서는 서비스 워커를 사용하여 초기 로드 후의 리소스들을 캐싱하고 클라이언트가 불필요한 요청을 하지 않도록 한다.
브라우저가 어느정도 추측하기는 하지만 사이트의 개발자는 일반적으로 어떤 리소스가 먼저 받아져야 하는 중요 리소스인지 알고 있다. 이를 preload
리소스 힌트를 사용하여 브라우저에게 중요 리소스를 직접 알려줄 수 있다.
특정 리소스에 힌트를 사용하여 알려줌으로 인해 브라우저가 따로 찾지 않아도 해당 리소스를 빠르게 로드할 수 있게 되는 것이다. preload는 현재 탐색 경로에 있어 중요한 리소스를 로드하는 데 걸리는 시간을 최적화하기 위한 좋은 방법이다.
preloading이 로딩 타임과 라운드트립을 줄이는 좋은 방법이긴 하지만, 너무 많은 파일을 이렇게 처리하면 문제가 될 수 있다. 브라우저 캐시에는 제한이 있고. 클라이언트가 실제로 사용하지 않는 리소스들에도 네트워크 대역폭을 낭비하게 된다.
PRPL패턴은 초기 로딩 속도를 최적화하는 데 초점이 맞춰져 있다. 처음 탐색된 페이지의 렌더링이 완료될 때 까지 다른 리소스들은 로드되지 않는다.
같은 목적을 달성하기 위해 코드 스플리팅을 적용하여 코드들을 작고 효율적으로 나눌 수 있다. 이렇게 나눠지는 번들은 클라이언트에서 필요로 할 때만 로드되며 캐시하기 용이해지도록 만드는 것이다.
그러나 크기가 큰 번들을 캐시하게 되면 문제가 발생할 수 있다. 그 문제는 여러 번들들이 같은 리소스를 공유할때 발생한다.
브라우저는 어떤 번들이 여러 라우팅 경로에서 공유되어 사용될 지 알 수 없기 때문에 이런 리소스들을 캐시할 수 없다. 하지만 리소스 캐싱은 라운드트립 최소화 뿐만 아니라 앱의 오프라인 처리 관련해서도 꼭 해결되어야 하는 문제이다.
PRPL패턴을 적용할 때는 요청되는 번들이 필요한 최소한의 리소스를 포함하며, 브라우저 캐시가 가능한지를 확인해야 한다. 달리 생각하면 결국 번들이 없는 것이 효율적이라는 이야기가 되며, 번들되지 않은 모듈로 조금더 심플하게 앱을 구성할 수 있다.
앱을 번들하여 동적으로 최소한의 리소스를 받아오게 하면 서버와 브라우저에 HTTP/2 를 적용하거나 리소스 캐시를 효과적으로 적용하는데 도움이 된다. 브라우저가 HTTP/2 push를 지원하지 않는 경우 별도의 빌드를 제공하여 라운드트립의 수를 줄일수도 있다. 클라이언트는 어떤 것이 받아지는지 알 필요 없이 서버가 브라우저에 맞춰 응답하는 것이다.
메인 엔트리 포인트에 앱 쉘을 사용하는 방법을 PRPL 패턴에서 종종 사용한다. 앱 쉘은 모든 라우팅 경로에서 사용하는 앱의 최소한의 로직들이다. 앱 쉘은 앱의 라우터를 포함하므로 동적으로 필요 리소스를 요청할수도 있다.
PRPL 패턴에서는 첫 탐색 페이지가 사용자의 단말에 보여지기 전 까지 다른 리소스를 추가적으로 받지 않는다. 초기 라우팅이 완료되면 서비스워커가 설치되어 백그라운드에서 자주 방문하는 경로의 리소스를 다운로드하게 된다.
데이터를 백그라운드에서 받기 때문에 사용자는 무언가 지연시간을 느끼지 않는다. 사용자가 서비스워커가 캐싱한 자주 방문했던 페이지로 이동하면. 서비스워커가 즉시 캐시된 컨텐츠를 받아 화면에 보여준다.
그 외에 자주 방문하지 않는 경로에 대한 리소스는 동적으로 가져오도록 구성하는 것이다.