Flywheel의 Rails에서 UI 구성 요소를 빌드하는 방법

게시 됨: 2019-11-16

대규모 웹 애플리케이션에서 시각적 일관성을 유지하는 것은 많은 조직에서 공유하는 문제입니다. Flywheel에서 우리도 다르지 않습니다. 우리의 주요 웹 애플리케이션은 Ruby on Rails로 구축되었으며 약 15명의 Rails 개발자와 3명의 프론트엔드 개발자가 주어진 날짜에 여기에 코드를 커밋합니다. 우리는 디자인에도 큰 관심을 가지고 있으며(이는 회사의 핵심 가치 중 하나입니다), 스크럼 팀에는 개발자와 매우 긴밀하게 협력하는 세 명의 디자이너가 있습니다.

우리의 주요 목표는 모든 플라이휠 개발자가 장애물 없이 반응형 페이지를 구축할 수 있도록 하는 것입니다. 일반적으로 로드블록에는 목업을 구축하는 데 사용할 기존 구성 요소(매우 유사하고 중복되는 구성 요소로 코드베이스를 부풀려짐)와 디자이너와 재사용 가능성에 대해 논의해야 할 때를 모르는 것이 포함됩니다. 이는 일관성 없는 고객 경험, 개발자의 불만, 개발자와 디자이너 간의 이질적인 디자인 언어에 기여합니다.

우리는 스타일 가이드와 UI 패턴 및 구성 요소를 구축/유지하는 방법을 여러 번 반복했으며 각 반복은 당시 우리가 직면한 문제를 해결하는 데 도움이 되었습니다. 우리는 이제 새로운 접근 방식을 취하고 있습니다. 이 접근 방식은 앞으로 오랫동안 우리를 설정해 줄 것이라고 확신합니다. Rails 애플리케이션에서 유사한 문제에 직면하고 서버 측에서 구성 요소에 접근하려는 경우 이 기사가 몇 가지 아이디어를 제공할 수 있기를 바랍니다.

이 기사에서는 다음 내용을 다룹니다.

  • 우리가 해결하는 것
  • 구속 구성요소
  • 서버 측의 렌더링 구성 요소
  • 서버 측 구성 요소를 사용할 수 없는 경우


우리가 해결하는 것

우리는 UI 구성 요소를 완전히 제한하고 동일한 UI가 한 가지 이상의 방식으로 생성될 가능성을 없애고 싶었습니다. 고객이 (처음에는) 구분하지 못할 수도 있지만 구성 요소에 대한 제약 조건이 없으면 개발자 환경이 혼란스러워지고 유지 관리가 매우 어렵고 전체적인 디자인 변경이 어려워집니다.

우리가 구성 요소에 접근하는 전통적인 방법은 주어진 구성 요소를 빌드하는 데 필요한 전체 마크업을 나열한 스타일 가이드를 사용하는 것이었습니다. 예를 들어, slat 구성 요소의 스타일 가이드 페이지는 다음과 같습니다.

이것은 잘 작동했고 몇 년 동안 우리에게 적합했지만 변형, 상태 또는 구성 요소를 사용하는 대체 방법을 추가했을 때 문제가 발생하기 시작했습니다. UI의 복잡한 부분을 사용하여 원하는 변형을 출력하기 위해 어떤 클래스를 사용하고 어떤 클래스를 피해야 하는지, 마크업이 어떤 순서로 있어야 하는지를 알기 위해 스타일 가이드를 참조하는 것이 번거로워졌습니다. 그리고 종종 디자이너는 주어진 구성 요소에 약간의 추가나 수정을 가했습니다. 스타일 가이드가 이를 충분히 지원하지 않았기 때문에 (다른 구성 요소의 일부를 부적절하게 잠식하는 것과 같이) 해당 조정이 올바르게 표시되도록 하는 대체 해킹이 짜증나게 보편화되었습니다.

구속되지 않은 구성요소의 예

시간이 지남에 따라 불일치가 어떻게 나타나는지 설명하기 위해 Flywheel 앱의 구성 요소 중 하나인 단순하고 인위적인 매우 일반적인 예인 카드 헤더를 사용합니다.

디자인 목업에서 새로 시작하는 카드 헤더는 이렇게 생겼습니다. 제목, 버튼, 하단 테두리로 아주 간단했습니다.

.card__header
  .card__header-left
    %h2 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

코딩한 후 제목 왼쪽에 아이콘을 추가하려는 디자이너를 상상해 보십시오. 기본적으로 아이콘과 제목 사이에는 여백이 없습니다.

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2 Backups
...

이상적으로는 카드 헤더용 CSS에서 해결해야 하지만 이 예에서는 다른 개발자가 “아, 알아요! 마진 도우미가 있습니다. 제목에 도우미 클래스를 때릴 것입니다.”

...
  .card__header-left
    = icon("arrow_backup", color: "gray25")
    %h2.--ml-10 Backups
...

글쎄, 그것은 기술적 으로 모형이 한 것처럼 보입니다. 맞습니까?! 물론입니다. 하지만 한 달 후 다른 개발자가 카드 헤더가 필요하지만 아이콘이 없다고 가정해 보겠습니다. 마지막 예제를 찾아 복사/붙여넣기한 다음 아이콘을 제거하기만 하면 됩니다.

다시 보니 맞는 것 같죠? 상황에 관계없이 디자인에 대한 예리한 눈이없는 사람에게는 물론입니다! 하지만 원본 옆에 보세요. 제목의 왼쪽 여백은 여백 왼쪽 도우미를 제거해야 한다는 것을 깨닫지 못했기 때문에 여전히 존재합니다!

이 예제에서 한 단계 더 나아가 하단 테두리가 없는 카드 헤더를 호출하는 또 다른 모형을 가정해 보겠습니다. 스타일 가이드에서 "테두리 없는" 상태를 찾아 적용할 수 있습니다. 완벽한!

그런 다음 다른 개발자가 해당 코드를 재사용하려고 할 수 있지만 이 경우 실제로 테두리가 필요합니다. 스타일 가이드에 문서화된 적절한 사용법을 무시하고 경계선 없는 클래스를 제거하면 경계선이 제공된다는 사실을 알지 못한다고 가정해 보겠습니다. 대신 수평선을 추가합니다. 제목과 테두리 사이에 추가 패딩이 있으므로 hr 및 voila에 도우미 클래스를 적용합니다!

원래 카드 헤더에 대한 이러한 모든 수정으로 인해 이제 코드에서 엉망이 되었습니다.

.card__header.--borderless
  .card__header-left
    %h2.--ml-10 Backups

  .card__header-right
    = link_to "#" do
      = icon("plus_small")

  %hr.--mt-0.--mb-0

위의 예는 제한되지 않은 구성 요소가 시간이 지남에 따라 어떻게 지저분해질 수 있는지에 대한 요점을 설명하기 위한 것임을 명심하십시오. 우리 팀의 누군가가 카드 헤더의 변형을 제공하려고 시도했다면 디자인 검토나 코드 검토에서 잡아야 합니다. 그러나 이와 같은 것은 때때로 균열을 통해 미끄러지기 때문에 방탄이 필요합니다!


구속 구성요소

위에 나열된 문제가 이미 구성 요소로 명확하게 해결되었다고 생각할 수 있습니다. 그것은 올바른 가정입니다! React 및 Vue와 같은 프론트엔드 프레임워크는 이러한 정확한 목적을 위해 매우 인기가 있습니다. UI를 캡슐화하는 놀라운 도구입니다. 그러나 우리가 항상 좋아하지 않는 문제가 하나 있습니다. 바로 UI가 JavaScript로 렌더링되어야 한다는 것입니다.

Flywheel 응용 프로그램은 주로 서버에서 렌더링된 HTML로 인해 백엔드가 매우 무겁습니다. 하지만 다행히도 구성 요소는 다양한 형태로 제공될 수 있습니다. 결국 UI 구성 요소는 브라우저에 마크업을 출력하는 스타일 및 디자인 규칙의 캡슐화입니다. 이러한 인식을 통해 구성 요소에 대해 동일한 접근 방식을 취할 수 있지만 JavaScript 프레임워크의 오버헤드가 없습니다.

아래에서 제한된 구성 요소를 구축하는 방법에 대해 알아볼 것이지만 다음은 이러한 구성 요소를 사용하여 찾은 몇 가지 이점입니다.

  • 구성 요소를 결합하는 잘못된 방법은 없습니다.
  • 구성 요소는 모든 디자인 사고를 수행합니다. (옵션을 전달하면 됩니다!)
  • 구성 요소를 만드는 구문은 매우 일관되고 추론하기 쉽습니다.
  • 구성 요소에 대한 설계 변경이 필요한 경우 구성 요소에서 한 번만 변경할 수 있으며 모든 곳에서 업데이트될 수 있습니다.

서버 측의 렌더링 구성 요소

구성 요소를 제한하여 무엇에 대해 이야기하고 있습니까? 파헤쳐보자!

앞서 언급했듯이 우리는 Flywheel 애플리케이션에서 작업하는 모든 개발자가 페이지의 디자인 모형을 보고 방해 없이 해당 페이지를 즉시 빌드할 수 있기를 바랍니다. 즉, UI를 만드는 방법은 A) 매우 잘 문서화되어야 하고 B) 매우 선언적이고 추측이 없어야 합니다.

구출에 대한 부분 (또는 우리는 그렇게 생각했습니다)

우리가 과거에 시도했던 이것에 대한 첫 번째 시도는 Rails 부분을 사용하는 것이었습니다. 부분은 템플릿에서 재사용을 위해 Rails가 제공하는 유일한 도구입니다. 당연히 모든 사람이 가장 먼저 찾는 대상입니다. 그러나 논리를 재사용 가능한 템플릿과 결합해야 하는 경우 부분을 사용하는 모든 컨트롤러에 논리를 복제하거나 논리를 부분 자체에 포함하는 두 가지 선택이 있기 때문에 논리에 의존하는 데는 상당한 단점이 있습니다.

부분은 복사/붙여넣기 복제 실수를 방지하고 무언가를 재사용해야 하는 처음 몇 번에는 제대로 작동합니다. 그러나 우리의 경험에 따르면 부분은 점점 더 많은 기능과 논리에 대한 지원으로 곧 복잡해집니다. 그러나 논리는 템플릿에 있으면 안 됩니다!

세포 소개

운 좋게도 코드 재사용하고 논리를 뷰에서 볼 수 없도록 하는 부분에 대한 더 나은 대안이 있습니다. Trailblazer에서 개발한 Ruby gem인 Cells입니다. 셀은 React 및 Vue와 같은 프론트엔드 프레임워크에서 인기를 얻기 훨씬 이전부터 존재했으며 이를 통해 로직 템플릿을 모두 처리하는 캡슐화된 뷰 모델을 작성할 수 있습니다. 그들은 Rails가 실제로 가지고 있지 않은 뷰 모델 추상화를 제공합니다. 우리는 실제로 Flywheel 앱에서 Cells를 얼마 동안 사용해 왔지만, 재사용이 가능한 전역적 규모는 아닙니다.

가장 단순한 수준에서 Cell을 사용하면 다음과 같이 마크업 덩어리를 추상화할 수 있습니다(템플릿 언어로 Haml을 사용합니다).

%div
  %h1 Hello, world!

재사용 가능한 뷰 모델(이 시점에서 부분과 매우 유사)으로 변환하고 다음과 같이 변경합니다.

= cell("hello_world")

이것은 궁극적으로 셀 자체를 수정하지 않고는 도우미 클래스나 잘못된 자식 구성 요소를 추가할 수 없는 부분으로 구성 요소를 제한하는 데 도움이 됩니다.

세포 구성

모든 UI 셀을 app/cells/ui 디렉토리에 넣습니다. 각 셀에는 _cell.rb 접미사가 붙은 하나의 Ruby 파일만 포함되어야 합니다. 기술적으로 content_tag 도우미를 사용하여 Ruby에서 바로 템플릿을 작성할 수 있지만 대부분의 Cell에는 구성 요소에서 명명한 폴더에 있는 해당 Haml 템플릿도 포함되어 있습니다.

논리가 없는 기본 셀은 다음과 같습니다.

// cells/ui/slat_cell.rb

module UI
  class SlatCell < ViewModel
    def show
    end
  end
end

show 메소드는 셀을 인스턴스화할 때 렌더링되는 것이며 셀과 동일한 이름을 가진 폴더에서 해당 show.haml 파일을 자동으로 찾습니다. 이 경우 app/cells/ui/slat입니다(모든 UI 셀 범위를 UI 모듈로 지정).

템플릿에서 셀에 전달된 옵션에 액세스할 수 있습니다. 예를 들어, 셀이 = cell("ui/slat", title: "Title", subtitle: "Subtitle", label: "Label")과 같은 보기에서 인스턴스화되면 options 개체를 통해 해당 옵션에 액세스할 수 있습니다.

// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      %h4= options[:title]
      %p= options[:subtitle]
      = icon(options[:icon], color: "blue")

옵션이 없을 때 빈 요소가 렌더링되는 것을 방지하기 위해 간단한 요소와 해당 값을 셀의 메서드로 이동하는 경우가 많습니다.

// cells/ui/slat_cell.rb
def title
  return unless options[:title]
  content_tag :h4, options[:title]
end

def subtitle
  return unless options[:subtitle]
  content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
  .slat__inner
    .slat__content
      = title
      = subtitle

UI 유틸리티로 셀 래핑

이것이 대규모로 작동할 수 있다는 개념을 증명한 후, 셀을 호출하는 데 필요한 외부 마크업을 해결하고 싶었습니다. 흐름이 정확하지 않고 기억하기 어렵습니다. 그래서 우리는 그것을 위해 작은 도우미를 만들었습니다! 이제 우리는 = ui "name_of_component"를 호출하고 옵션을 인라인으로 전달할 수 있습니다.

= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"

인라인 대신 블록으로 옵션 전달

UI 유틸리티를 조금 더 발전시키면 한 줄에 많은 옵션이 있는 셀이 따라가기가 매우 어렵고 보기 흉할 것이라는 사실이 금세 명백해졌습니다. 다음은 인라인으로 정의된 많은 옵션이 있는 셀의 예입니다.

= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]

이는 매우 번거롭기 때문에 Cell setter 메서드를 가로채서 이를 해시 값으로 변환한 다음 옵션으로 병합하는 OptionProxy라는 클래스를 생성해야 합니다. 그것이 복잡하게 들리더라도 걱정하지 마십시오. 저에게도 복잡합니다. 다음은 선임 소프트웨어 엔지니어 중 한 명인 Adam이 작성한 OptionProxy 클래스의 요지입니다.

다음은 셀 내부에서 OptionProxy 클래스를 사용하는 예입니다.

module UI
  class SlatCell < ViewModel
    def show
      OptionProxy.new(self).yield!(options, &block)
      super()
    end
  end
end

이제 그 자리에 있으면 성가신 인라인 옵션을 더 즐거운 블록으로 바꿀 수 있습니다!

= ui "slat" do |slat|
  - slat.title = "Title"
  - slat.subtitle = "Subtitle"
  - slat.label = "Label"
  - slat.link = "#"
  - slat.tertiary_title = "Tertiary"
  - slat.disabled = true
  - slat.checklist = ["Item 1", "Item 2", "Item 3"]

논리 소개

지금까지는 보기에 표시되는 내용에 대한 논리가 예제에 포함되지 않았습니다. 그것이 Cells가 제공하는 최고의 것 중 하나이므로 그것에 대해 이야기합시다!

Slat 구성 요소를 고수하면서 링크 옵션이 있는지 여부에 따라 전체를 링크로 렌더링하고 때로는 div로 렌더링해야 할 필요가 있습니다. 이것이 div 또는 링크로 렌더링할 수 있는 유일한 구성 요소라고 생각하지만 Cells의 힘에 대한 꽤 깔끔한 예입니다.

아래 메서드는 [:link] 옵션의 존재 여부에 따라 link_to 또는 content_tag 도우미를 호출합니다.

참고: 이것은 Cells를 사용하여 이 전체 UI 개발 방법을 구축하는 데 큰 영향을 미친 Adam Lassek에 의해 영감을 받아 작성되었습니다.

def container(&block)
  tag =
    if options[:link]
      [:link_to, options[:link]]
    else
      [:content_tag, :div]
    end

  send(*tag, class: “slat__inner”, &block)
end

이를 통해 템플릿의 .slat__inner 요소를 컨테이너 블록으로 교체할 수 있습니다.

.slat
  = container do
  ...

우리가 많이 사용하는 Cell의 로직의 또 다른 예는 조건부로 클래스를 출력하는 것입니다. 비활성화된 옵션을 셀에 추가한다고 가정해 보겠습니다. 이제 disabled: true 옵션을 전달하고 모든 것이 비활성화된 상태(클릭할 수 없는 링크로 회색으로 표시됨)로 바뀌는 것을 볼 수 있다는 점 외에는 셀 호출에서 다른 것은 변경되지 않습니다.

= ui "slat" do |slat|
  ...
  - slat.disabled = true

disabled 옵션이 true이면 원하는 비활성화된 모양을 얻는 데 필요한 템플릿의 요소에 클래스를 설정할 수 있습니다.

.slat{ class: possible_classes("--disabled": options[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
      %p{ class: possible_classes("--alt": options[:disabled]) }=
      options[:subtitle]
      = icon(options[:icon], color: "gray")

전통적으로 우리는 비활성화된 상태에서 모든 것이 올바르게 작동하도록 하기 위해 추가 클래스가 필요한 개별 요소를 기억(또는 스타일 가이드 참조)해야 했습니다. 세포를 사용하면 하나의 옵션을 선언한 다음 우리를 위해 무거운 작업을 수행할 수 있습니다.

참고: possible_classes는 Haml에서 조건부로 클래스를 좋은 방식으로 적용할 수 있도록 만든 메서드입니다.


서버 측 구성 요소를 사용할 수 없는 경우

세포 접근 방식은 우리의 특정 응용 프로그램과 우리가 일하는 방식에 매우 도움이 되지만, 우리 문제의 100%를 해결했다고 말하기는 부적절합니다. 우리는 여전히 JavaScript(많은 부분)를 작성하고 앱 전체에 걸쳐 Vue에서 꽤 많은 경험을 구축합니다. 시간의 75%는 Vue 템플릿이 여전히 Haml에 있고 Vue 인스턴스를 포함하는 요소에 바인딩하므로 여전히 셀 접근 방식을 활용할 수 있습니다.

그러나 구성 요소를 단일 파일 Vue 인스턴스로 완전히 제한하는 것이 더 합리적인 곳에서는 Cell을 사용할 수 없습니다. 예를 들어 우리의 선택 목록은 모두 Vue입니다. 하지만 괜찮은 것 같아요! Cell과 Vue 구성 요소 모두에서 구성 요소의 복제 버전이 필요하지 않았으므로 일부 구성 요소는 100% Vue로 빌드되고 일부는 Cell로 만들어도 괜찮습니다. 구성 요소가 Vue로 빌드된 경우 DOM에서 구성 요소를 빌드하려면 JavaScript가 필요하며 이를 위해 Vue 프레임워크를 활용합니다. 하지만 대부분의 다른 구성 요소의 경우 JavaScript가 필요하지 않으며 JavaScript가 필요한 경우 DOM이 이미 빌드되어 있어야 하며 이벤트 리스너를 연결하고 추가하기만 하면 됩니다.

세포 접근 방식을 계속 진행하면서 우리는 구성 요소를 만들고 사용하는 단 하나의 방법을 갖도록 셀 구성 요소와 Vue 구성 요소의 조합을 확실히 실험할 것입니다. 아직 어떻게 생겼는지 모르겠으니 도착하면 저 다리를 건너자!


우리의 결론

지금까지 가장 많이 사용되는 시각적 구성 요소 중 약 30개를 Cell로 변환했습니다. 그것은 우리에게 엄청난 생산성을 제공하고 개발자들에게 그들이 구축하고 있는 경험이 정확하고 함께 해킹되지 않았음을 검증하는 감각을 줍니다.

우리 디자인 팀은 우리 앱의 구성 요소와 경험이 Adobe XD에서 디자인한 것과 1:1이라는 확신을 가지고 있습니다. 구성 요소에 대한 변경 또는 추가는 이제 디자이너 및 프론트 엔드 개발자와의 상호 작용을 통해서만 처리되므로 나머지 팀은 디자인 목업과 일치하도록 구성 요소를 조정하는 방법을 아는 데 집중하고 걱정할 필요가 없습니다.

우리는 UI 구성 요소를 제한하는 접근 방식을 지속적으로 반복하고 있지만 이 기사에 설명된 기술을 통해 우리에게 잘 맞는 방법을 엿볼 수 있기를 바랍니다!


플라이휠에서 일하세요!

Flywheel에서 각 부서는 고객과 수익에 의미 있는 영향을 미칩니다. 고객 지원, 소프트웨어 개발, 마케팅 또는 그 사이의 어떤 것이든, 우리는 모두 사람들이 진정으로 사랑에 빠질 수 있는 호스팅 회사를 구축한다는 우리의 사명을 위해 함께 노력하고 있습니다.

우리 팀에 합류할 준비가 되셨습니까? 우리는 고용 중이다! 여기에서 신청하세요.