[A/B Testing] 실험 변수에 따라 부모 CSS 클래스 변경

실험을 진행하려는 기존 테마가 있습니다. 예를 들어, 주제 목록 보기의 다양한 스타일을 실험하는 것이 해당됩니다. 이를 위해 Google Optimize A/B 테스트를 사용하고 있습니다. 현재 계획은 사용자 중 50%에게는 변경 사항 없이 기존 테마를, 나머지 50%에게는 업데이트된 테마를 보여주는 것입니다. 실험 대상이 소규모이므로 원본과 변형본은 동일한 테마 내에서 정의되어야 합니다. 문제는 상위 CSS 클래스들이 동일한 이름을 공유하고 있는데, 특정 변수를 기반으로 이러한 상위 클래스에 스타일을 적용하고 싶습니다. 상위 클래스 이름이 동일한 서로 다른 템플릿에 어떻게 별도의 스타일을 적용할 수 있을까요?

아래는 우리가 달성하려는 사항을 단순화한 시나리오입니다.

common/common.scss에서 주제 목록 스타일이 다음과 같이 정의된 테마를 가지고 있다고 가정해 보겠습니다.

.topic-list {
  tbody {
    border: none;
  }
  tr {
    border: none;
  }
  .example-div {
    border: none;
  }
}

그리고 변형본(variant)에서는 이를 다음과 같이 설정하려고 합니다:

.topic-list {
  tbody {
    display: block;
    tr {
      display: block;
    }
  }
  .example-div {
    border: 2px solid black;
  }
  .another-example-div { 
    font-size: 10px;
  }
}

오버라이드되는 핸들바(handlebar) 파일은 다음과 같은 형태를 가지고 있습니다.

javascripts/discourse/templates/list/custom-topic-list-item.hbr

<div class=example-div'>
  Hello
</div>

javascripts/discourse/templates/list/variant-custom-topic-list-item.hbr

<div class=example-div'>
  Hello,
  <div class=another-example-div'>
    Nice to meet you
  </div>
</div>

topic-list 클래스 이름은 discourse 컴포넌트에 언급되어 있으며, 렌더링 후 구조는 다음과 같습니다.

<table class="topic-list">
  <tr class="topic-list-item">
   <div class=example-div'>
      Hello
    </div>
  </tr>
</table>

저는 헤더 파일 common/header.html에서 사용자 ID에 따라 이 파일 중 하나를 렌더링하고 싶습니다.

const { findRawTemplate } = require("discourse-common/lib/raw-templates");
const user_id = api.getCurrentUser().id
 
api.modifyClass('component:topic-list-item', {
  renderTopicListItem() {
    let template = findRawTemplate("list/custom-topic-list-item");
    if (user_id % 2 === 0)
       template = findRawTemplate("list/variant-custom-topic-list-item");
    if (template) {
      this.set("topicListItemContents", template(this).htmlSafe());
    }
  },
});

감사합니다

로그인하지 않은 사용자에게 포착되지 않은 에러(uncaught error)를 발생시킬 가능성이 높다는 점에 유의하세요. 아래처럼 수정하는 것을 권장합니다.

const user = api.getCurrentUser();
if (!user) return;

const userId = user.id;

이 문제를 해결했으니, 이제 질문으로 돌아가 보겠습니다.

CSS와 달리 SCSS는 부모 선택자를 지원하므로, 이를 활용할 수 있습니다.

모든 짝수 번째 사용자(id % 2)에게 적용하려는 것이므로

앱이 로드될 때 이를 확인할 수 있습니다. 따라서 맨 위에 다음과 같은 코드를 추가할 수 있습니다.

const user = api.getCurrentUser();
const userId = user.id;

if (!userId) return;

const testThemeUser = userId % 2 === 0;

if (testThemeUser) {
  document.body.classList.add("test-theme");
}

그 후 JS 조건문에서는 testThemeUser를, 스타일에서는 test-theme CSS 클래스를 사용할 수 있습니다.

즉, 다음과 같이 하면 됩니다.

JS

let template = findRawTemplate("list/custom-topic-list-item");
if (testThemeUser)
  template = findRawTemplate("list/variant-custom-topic-list-item");
if (template) {
  this.set("topicListItemContents", template(this).htmlSafe());
}

SCSS

.topic-list {
  tbody {
    border: none;
  }
  tr {
    border: none;
  }
  .example-div {
    border: none;
  }
  // test theme SCSS
  .test-theme & {
    .example-div {
      border: 2px solid black;
    }
    .another-example-div {
      font-size: 10px;
    }
  }
}

답변해 주셔서 감사합니다 @Johani
현재 저는 topic-list라는 특정 컴포넌트 클래스만 업데이트해야 하므로 이 접근 방식을 사용하고 있습니다.

const useTestTheme = userId % 2 === 0

api.modifyClass('component:topic-list', {
    classNameBindings: ["test"],
    test: useTestTheme,
})