Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

단일 페이지 앱(SPA)을 버리고 Turbolinks를 선택해야 하는 이유

Rails 개발자들에게 Turbolinks는 아마 가장 미움받는 단어 중 하나일 겁니다.

한 번쯤 시도해 보셨을지도 모릅니다. 새 프로젝트나 기존 애플리케이션에 turbolinks를 추가했는데, 곧 앱이 이상하고 기묘한 방식으로 오작동하기 시작하죠. 다행히 해결책은 간단했습니다. 바로 turbolinks를 끄는 것.

...하지만 잘 활용하는 회사들도 있습니다. Honeybadger에서도 성공적으로 사용하고 있는데, 우리가 천재여서 그런 건 아닙니다.

답은 너무나 단순해서 소개하기가 망설여질 정도입니다. 하지만 Ruby Nation과 Madison+Ruby에서 이 주제로 발표를 해보니 생각보다 많은 분들이 유용하다고 느끼더군요. 그럼 지금부터 자세히 살펴보겠습니다.

단일 페이지 앱(SPA)을 버리고 Turbolinks를 선택해야 하는 이유

Turbolinks와 PJAX는 본질적으로 같은 방식으로 작동합니다. 너무나 비슷하기 때문에 이 글에서는 편의상 PJAX라고만 부르겠습니다. :)

PJAX는 두 번의 페이지 요청으로 이해할 수 있습니다. 사용자가 처음 페이지를 요청하면 일반적인 '전통적인' Rails 페이지처럼 응답됩니다. 하지만 사용자가 PJAX가 적용된 링크를 클릭하면 특별한 일이 일어납니다. 페이지 전체를 다시 로드하는 대신, AJAX를 통해 페이지의 일부분만 업데이트되는 것이죠.

이렇게 하면 단일 페이지 앱의 장점을 상당 부분 누리면서도 몇 가지 어려움은 피할 수 있습니다:

  • 매 요청마다 페이지 전체를 다시 로드하지 않으므로, PJAX 앱은 SPA 못지않게 빠른 반응성을 보여줍니다.
  • 프론트엔드와 백엔드에 동일한 기술 스택을 사용할 수 있습니다.
  • 사용자가 JavaScript를 비활성화해도 PJAX 앱은 기본적으로 우아하게 동작이 저하됩니다(degrade gracefully).
  • PJAX 앱은 접근성과 SEO 친화성을 확보하기가 더 쉽습니다.

PJAX의 무거운 작업을 대신 처리해 주는 라이브러리는 여러 가지가 있습니다. 그중 가장 널리 알려진 것이 Turbolinks일 겁니다. 설정도 아주 간단합니다. Gemfile에 turbolinks 젬을 추가하기만 하면 됩니다:

gem 'turbolinks'

그리고 app/assets/javascripts/application.js에 JS를 포함시킵니다:

//= require turbolinks

이제 앱을 다시 로드하면 모든 내부 링크가 turbolink가 됩니다. 링크를 클릭하면 새 페이지가 AJAX로 요청되어 현재 문서에 삽입됩니다.

jquery-pjax 구현하기

Honeybadger에서는 GitHub이 처음 개발한 PJAX 라이브러리를 사용합니다. Turbolinks보다 설정이 조금 더 필요하지만, 그만큼 훨씬 유연합니다.

모든 링크가 PJAX라고 가정하는 대신, 어떤 링크를 PJAX로 처리할지 직접 제어할 수 있습니다. PJAX 콘텐츠가 페이지의 어느 위치에 삽입될지도 지정할 수 있죠.

먼저 HTML에 컨테이너를 추가해야 합니다:

<div class="container" id="pjax-container">
  Go to <a href="/page/2">next page</a>.
</div>

다음으로 PJAX 링크를 설정합니다:

$(document).pjax('a', '#pjax-container')

마지막으로 Rails에게 PJAX 요청 시에는 레이아웃을 렌더링하지 말라고 알려줘야 합니다. 이 과정을 생략하면 헤더와 푸터가 중복됩니다. 즉, 웹사이트가 영화 '인셉션'처럼 겹겹이 중첩된 모습이 되어 버립니다.

def index
  if request.headers['X-PJAX']
    render :layout => false
  end
end

사실 그렇게 쉽지만은 않습니다!

좋습니다. 사실 제가 말한 것보다는 조금 더 복잡합니다. 거의 아무도 언급하지 않는 거대한 함정이 하나 있기 때문입니다.

페이지가 로드될 때마다 DOM이 초기화되지 않는다는 것은, 기존 Rails 앱에서 멀쩡히 작동하던 JS가 매우 기묘한 방식으로 깨질 수 있다는 뜻입니다.

그 이유는 우리 대부분이 실수로 이름 충돌을 일으키기 쉬운 방식으로 JS를 작성하도록 배웠기 때문입니다. 가장 교활한 범인 중 하나는 바로 단순한 jQuery 셀렉터입니다.

// 나는 무해해 보이지만, 결코 그렇지 않다!
$(".something")

새로고침되지 않는 페이지를 위한 JS 작성법

새로고침되지 않는 페이지용 JS를 작성할 때 충돌은 최우선 문제입니다. 가장 기묘하고 디버깅하기 어려운 문제들은 JS가 본래 손대지 말았어야 할 HTML을 조작할 때 발생합니다. 예를 들어 평범한 jQuery 이벤트 핸들러를 살펴보겠습니다:

$(document).on("click", ".hide-form", function() {
  $(".the-form").hide();
});

이 코드는 한 페이지에서만 실행된다면 완전히 합리적입니다. 하지만 DOM이 다시 로드되지 않는다면, 언젠가 누군가 .hide-form 클래스를 가진 또 다른 요소를 추가할 가능성을 열어두는 셈입니다. 그 순간 충돌이 발생합니다.

이런 종류의 충돌은 전역 참조(global reference)가 많을 때 일어납니다. 그렇다면 전역 참조의 수를 줄이려면 어떻게 해야 할까요? 네임스페이스를 사용하면 됩니다.

셀렉터에 네임스페이스 적용하기

아래 Ruby 클래스에서는 단 하나의 전역 이름(클래스 이름)만으로 수많은 메서드 이름을 감추고 있습니다.

# 하나의 전역 이름이 두 메서드 이름을 감춘다
class MyClass
  def method1
  end

  def method2
  end
end

DOM 요소를 위한 내장 네임스페이스 지원은 아직 없지만(적어도 ES6 WebComponents가 보편화되기 전까지는), 코딩 컨벤션으로 네임스페이스를 흉내 낼 수 있습니다.

예를 들어 태그 편집 위젯을 구현한다고 상상해 봅시다. 네임스페이싱 없이는 다음과 같을 겁니다. 전역 참조가 세 개라는 점에 주목하세요.

// <input class="tags" type="text" />
// <button class="tag-submit">Save</button>
// <span class="tag-status"></span>

$(".tag-submit").click(function(){
  save($(".tags").val());
  $(".tag-status").html("Tags were saved");
});

하지만 '네임스페이스' 역할을 하는 컨테이너를 만들고 모든 요소 조회를 그 안에서 상대적으로 수행하면, 전역 참조를 단 하나로 줄일 수 있습니다. 클래스 몇 개도 아예 없앨 수 있죠.

// <div class="tags-component">
//   <input type="text" />
//   <button>Save</button>
//   <span></span>
// </div>

$container = $("#tags-component")
$container.on("click", "button", function(){
  save($container.find("input").val());
  $container.find("span").html("Tags were saved");
});

제가 말했듯이, 정말 간단합니다.

위 예제에서 저는 DOM 요소들을 "tags-component"라는 클래스를 가진 컨테이너 안에 넣음으로써 네임스페이스를 만들었습니다. 그 이름 자체에는 특별한 의미가 없습니다. 하지만 모든 네임스페이스 컨테이너가 "-component"로 끝나는 클래스를 갖는 명명 규칙을 채택하면, 흥미로운 일들이 벌어지기 시작합니다.

잘못된 전역 셀렉터를 한눈에 알아볼 수 있습니다

허용되는 전역 셀렉터가 컴포넌트뿐이고, 모든 컴포넌트가 "-component"로 끝나는 클래스를 가진다면, 나쁜 전역 셀렉터가 있는지 한눈에 파악할 수 있습니다.

// 나쁜 예
$(".foo").blah()

// 좋은 예
$(".foo-component").blah()

HTML을 제어하는 JS를 찾기가 쉬워집니다

인터랙티브한 태그 폼으로 다시 돌아가 봅시다. 폼의 HTML을 작성했다면, 이제 JS와 CSS를 추가해야 합니다. 그런데 그 파일들을 어디에 두어야 할까요? 다행히 네임스페이스 명명 규칙이 있다면, 그것이 JS와 CSS 파일의 명명 규칙으로 자연스럽게 확장됩니다. 디렉터리 구조는 다음과 같을 수 있습니다.

.
├── javascripts
|   ├── application.coffee
│   └── components
│       └── tags.coffee
└── stylesheets
    ├── application.scss
    └── components
        └── tags.scss

자동 초기화

전통적인 웹 앱에서는 페이지 로드 시 모든 JS를 초기화하는 것이 일반적입니다. 하지만 PJAX와 Turbolinks 환경에서는 DOM에 요소가 끊임없이 추가되고 제거됩니다. 따라서 코드가 새 컴포넌트가 DOM에 진입하는 시점을 자동으로 감지하고, 필요한 JS를 즉석에서 초기화할 수 있다면 큰 이점이 있습니다.

일관된 명명 규칙이 있으면 이 작업이 놀랍도록 쉬워집니다. 자동 초기화에는 무수한 접근 방식이 있을 수 있는데, 그중 하나는 다음과 같습니다:

Initializers = {
  tags: function(el){
    $(el).on("click", "button", function(){  
      // 컴포넌트 설정
    });
  }

  // 다른 이니셜라이저도 여기에 추가할 수 있습니다
}

// 일반 및 pjax 페이지 로드 시마다 호출되는 핸들러
$(document).on("load, pjax:load", function(){
  for(var key in Initializers){
    $("." + key + "-component").each(Initializers[key]);
  }
}

CSS까지 더 좋아집니다!

웹 페이지에서 충돌의 원인은 JavaScript만이 아닙니다. CSS는 그보다 더 심각할 수 있습니다! 다행히 우리의 멋진 네임스페이싱 시스템 덕분에 충돌 없이 CSS를 작성하기가 훨씬 쉬워집니다. SCSS에서는 더욱 편리합니다:

.tags-component {
  input { ... }
  button { ... }
  span { ... }
}