Rails에서 UI 구성요소를 구축하는 방법

게시 됨: 2024-06-28

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

두 사람이 웹사이트 디자인을 공동 작업하고 있습니다.

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

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

수염을 기른 ​​남자가 코드 줄을 표시하는 컴퓨터 모니터 앞에 앉아 카메라를 향해 미소를 짓고 있습니다.

이 기사에서는 다음 내용을 자세히 살펴보겠습니다.

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

우리가 해결하고 있는 것

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

우리가 컴포넌트에 접근하는 전통적인 방식은 특정 컴포넌트를 구축하는 데 필요한 전체 마크업을 나열하는 스타일 가이드를 이용하는 것이었습니다. 예를 들어, slat 구성 요소에 대한 스타일 가이드 페이지는 다음과 같습니다.

슬레이트 구성 요소에 대한 스타일 가이드 페이지

이는 몇 년 동안 잘 작동했지만 구성 요소를 사용하는 변형, 상태 또는 대체 방법을 추가하면 문제가 발생하기 시작했습니다. 복잡한 UI 부분에서는 사용할 클래스와 피해야 할 클래스, 원하는 변형을 출력하기 위해 필요한 마크업 순서를 파악하기 위해 스타일 가이드를 참조하는 것이 번거롭습니다.

종종 디자이너는 특정 구성 요소에 약간의 추가나 조정을 가합니다. 스타일 가이드가 이를 지원하지 않았기 때문에 해당 조정을 올바르게 표시하도록 하는 대체 해킹(예: 다른 구성 요소의 일부를 부적절하게 잠식하는 것)이 짜증날 정도로 일반화되었습니다.

구속되지 않은 구성요소 예

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

디자인 모형에서 새롭게 시작한 카드 헤더의 모습은 다음과 같습니다. 제목, 버튼, 하단 테두리로 매우 간단했습니다.

 .card__헤더
  .card__header-왼쪽
    %h2 백업
  .card__header-오른쪽
    = link_to "#" do
      = icon("plus_small")

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

 ...
  .card__header-왼쪽
    = icon("arrow_backup", 색상: "회색25")
    %h2 백업
...

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

 ...
  .card__header-왼쪽
    = icon("arrow_backup", 색상: "회색25")
    %h2.--ml-10 백업
...

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

다시 보니 맞는 것 같죠? 맥락에서 벗어나 디자인에 대한 예리한 안목이 없는 사람에게는 물론입니다! 그런데 원본 옆에 보세요. 제목의 왼쪽 여백은 제거해야 하는 여백 왼쪽 도우미를 인식하지 못했기 때문에 여전히 존재합니다!

이 예를 한 단계 더 발전시켜 아래쪽 테두리가 없는 카드 헤더를 요구하는 또 다른 모형을 가정해 보겠습니다. 스타일 가이드에서 "경계 없음"이라는 상태를 찾아서 적용할 수도 있습니다. 완벽한!

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

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

 .card__header.--경계 없음
  .card__header-왼쪽
    %h2.--ml-10 백업
  .card__header-오른쪽
    = link_to "#" do
      = icon("plus_small")
  %hr.--mt-0.--mb-0

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


구성요소 구속

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

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

아래에서 제한된 구성요소를 구축하는 방법에 대해 알아보겠습니다. 이를 사용하여 찾은 몇 가지 이점은 다음과 같습니다.

  • 구성요소를 결합하는 데 있어 잘못된 방법은 결코 없습니다.
  • 구성 요소는 사용자를 위해 모든 디자인 사고를 수행합니다. (옵션만 전달하면 됩니다!)
  • 구성 요소를 생성하는 구문은 매우 일관되고 추론하기 쉽습니다.
  • 구성 요소에 디자인 변경이 필요한 경우 구성 요소에서 한 번만 변경하면 모든 곳에서 업데이트된다는 확신을 가질 수 있습니다.

서버 측에서 구성 요소 렌더링

그렇다면 구성요소를 제한한다는 것은 무엇을 말하는 것일까요? 파헤쳐보자!

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

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

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

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

세포 소개

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

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

 %div
  %h1 안녕하세요, 세상!

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

 = 셀("hello_world")

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

세포 구성

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

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

 // 셀/ui/slat_cell.rb
모듈 UI
  클래스 SlatCell < ViewModel
    데프 쇼
    끝
  끝
끝

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

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

 // 셀/ui/slat/show.haml
.스키
  .slat__inner
    .slat__content
      %h4= 옵션[:제목]
      %p= 옵션[:자막]
      = icon(옵션[:아이콘], 색상: "파란색")

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

 // 셀/ui/slat_cell.rb
정의 제목
  옵션[:제목]이 아닌 경우 반환
  content_tag :h4, 옵션[:제목]
끝
데프 자막
  옵션[:자막]이 아니면 반환
  content_tag :p, 옵션[:자막]
끝
 // 셀/ui/slat/show.haml
.스키
  .slat__inner
    .slat__content
      = 제목
      = 자막

UI 유틸리티를 사용하여 셀 래핑

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

 = ui "slat", 제목: "제목", 자막: "자막", 라벨: "라벨"

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

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

 = ui “slat", 제목: “제목”, 자막: “Subtitle”, 라벨: “라벨”, 링크: “#”, tertiary_title: “3차”, 비활성화: true, 체크리스트: [“항목 1”, “항목 2”, “항목 3”]

이는 매우 번거로운 작업이므로 Cells setter 메서드를 가로채서 이를 해시 값으로 변환한 다음 옵션으로 병합하는 OptionProxy라는 클래스를 생성하게 됩니다. 복잡하게 들리더라도 걱정하지 마세요. 나에게도 복잡합니다. 다음은 수석 소프트웨어 엔지니어 중 한 명인 Adam이 작성한 OptionProxy 클래스의 요지입니다.

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

 모듈 UI
  클래스 SlatCell < ViewModel
    데프 쇼
      OptionProxy.new(self).yield!(옵션, &차단)
      감독자()
    끝
  끝
끝

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

 = ui "slat" do |slat|
  - slat.title = "제목"
  - slat.subtitle = "자막"
  - slat.label = "라벨"
  - slat.link = "#"
  - slat.tertiary_title = "3차"
  - slat.disabled = true
  - slat.checklist = ["항목 1", "항목 2", "항목 3"]

로직 소개

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

slat 구성 요소를 고수하면서 링크 옵션이 있는지 여부에 따라 때로는 전체를 링크로 렌더링하고 때로는 div로 렌더링해야 합니다. 저는 이것이 div나 링크로 렌더링될 수 있는 유일한 구성 요소라고 생각합니다. 하지만 이는 Cells의 강력한 기능을 보여주는 매우 깔끔한 예입니다.

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

 def 컨테이너(&블록)
  태그 =
    if 옵션[:링크]
      [:link_to, 옵션[:link]]
    또 다른
      [:content_tag, :div]
    끝
  send(*태그, 클래스: “slat__inner”, &block)
끝

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

 .스키
  = 컨테이너 할
  ...

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

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

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

 .slat{ 클래스: available_classes("--disabled": 옵션[:disabled]) }
  .slat__inner
    .slat__content
      %h4{ 클래스: available_classes("--alt": 옵션[:disabled]) }= 옵션[:제목]
      %p{ 클래스: available_classes("--alt": 옵션[:disabled]) }=
      옵션[:자막]
      = icon(옵션[:아이콘], 색상: "회색")

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

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


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

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

그러나 구성 요소를 단일 파일 Vue 인스턴스로 완전히 제한하는 것이 더 적합한 곳에서는 Cells를 사용할 수 없습니다. 예를 들어 선택 목록은 모두 Vue입니다. 하지만 괜찮은 것 같아요! Cells와 Vue 구성 요소 모두에 중복 버전의 구성 요소가 필요하지 않으므로 일부 구성 요소는 100% Vue로 구축되고 일부 구성 요소는 Cells로 구성되어도 괜찮습니다.

구성 요소가 Vue로 빌드된 경우 DOM에서 구성 요소를 빌드하려면 JavaScript가 필요하며 이를 위해 Vue 프레임워크를 활용합니다. 하지만 대부분의 다른 구성 요소에는 JavaScript가 필요하지 않으며, 필요한 경우 DOM이 이미 구축되어 있어야 하며 연결하고 이벤트 리스너를 추가하기만 하면 됩니다.

셀 접근 방식을 계속 진행하면서 우리는 셀 구성 요소와 Vue 구성 요소의 조합을 실험하여 구성 요소를 생성하고 사용하는 단 하나의 방법을 갖게 될 것입니다. 아직은 그게 어떤 모습일지 모르니까, 거기 도착하면 저 다리를 건너도록 할게요!


우리의 결론

지금까지 가장 많이 사용되는 시각적 구성 요소 중 약 30개를 셀로 변환했습니다. 이를 통해 생산성이 크게 향상되었으며 개발자는 자신이 구축하고 있는 경험이 정확하고 함께 해킹되지 않았다는 확신을 갖게 되었습니다.

우리 디자인 팀은 앱의 구성 요소와 경험이 Adobe XD에서 디자인한 것과 1:1로 일치한다는 점을 그 어느 때보다 확신하고 있습니다. 구성 요소에 대한 변경 또는 추가는 이제 디자이너 및 프런트 엔드 개발자와의 상호 작용을 통해서만 처리되므로 나머지 팀은 디자인 모형에 맞게 구성 요소를 조정하는 방법을 걱정하지 않고 집중할 수 있습니다.

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


우리와 함께 일하세요!

우리 제품을 담당하는 모든 부서는 고객과 수익에 의미 있는 영향을 미칩니다. 고객 지원, 소프트웨어 개발, 마케팅 등 무엇이든 우리는 사람들이 진정으로 사랑에 빠질 수 있는 호스팅 회사를 만들기 위한 사명을 위해 함께 노력하고 있습니다.

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