사용자 정의 테마 컴포넌트 (가이드)

제 포럼에서 기본 테마에 대해 사용자에게 몇 가지 커스터마이징 옵션을 제공하고자 했습니다. 구체적으로 포럼 배경을 설정할 수 있는 옵션을 제공하려 했습니다. 이를 위해 서로 다른 테마를 만들 수도 있지만, 이는 상당한 추가 작업이 필요합니다. 또한 테마에는 설정이 있지만, 이는 모든 사용자에게 적용되며 특정 사용자를 위한 것이 아닙니다. 하지만 테마에 대해 사용자 설정 가능한 옵션을 구현하는 방법이 있습니다. 바로 사용자 필드를 '악용’하는 것입니다.

제 테마에서 사용자는 실제로 벽지(이미지, 배경색 혼합, 콘텐츠 투명도)에 대해 3가지 설정을 가지고 있지만, 이 글에서는 단일 설정인 벽지 이미지에만 집중하겠습니다.

단계 1: 사용자 필드 생성

사용자는 다양한 벽지 중 하나를 선택하거나 아예 벽지를 사용하지 않을 수 있는 옵션을 가집니다. 사용자 필드에는 다음과 같은 옵션을 가진 선택 가능한 드롭다운 필드를 생성했습니다:

  1. none – 아예 벽지를 사용하지 않음, 이는 중요합니다.
  2. Default – 사용자가 선택을 하지 않은 경우
  3. Alternative One
  4. Alternative Two

이 필드는 선택 사항(optional)이므로 기본값은 빈 값입니다. 빈 값인 경우 기본 벽지가 사용되도록 하였습니다.

단계 2: CSS

common/common.scss 파일에서 다음을 정의했습니다:

:root {
  --wallpaper-default: url(#{$img-wallpaper-default});
  --wallpaper-alternative-one: url(#{$img-wallpaper-alternative-one});
  --wallpaper-alternative-two: url(#{$img-wallpaper-alternative-two});
  /* 초기 이미지 전환 가능성을 줄이기 위해 기본값을 none으로 설정 */
  --wallpaper: none;
}

body {
  background-image: var(--wallpaper);
}

:root에는 다양한 벽지 이미지에 대한 변수가 포함되어 있으며, 이들은 테마 자산입니다. 배경 이미지는 --wallpaper 변수에 설정되며, 초기 상태에서는 none입니다. 따라서 기본적으로 CSS가 로드될 때 벽지는 없습니다. 이는 사용자가 원래와 다른 벽지를 선택했을 때 발생할 수 있는 "플리커(화면 깜빡임)"를 제거하기 위한 것입니다.

각각의 선택 가능한 벽지에는 --wallpaper-xx-yy-zz 변수가 할당됩니다. 사용자 필드 값은 일반 텍스트입니다. 이 값에 따라 배경 이미지로 사용할 올바른 CSS 변수를 선택합니다.

단계 3: 테마 설정

JavaScript 코드에서는 벽지 설정을 포함하는 사용자 필드를 알아야 합니다. 이를 얻는 가장 쉬운 방법은 사용자 필드 ID를 직접 참조하는 것입니다. 따라서 이 사용자 필드 ID를 구성하기 위해 테마 설정을 생성해야 합니다.

wallpaper_user_field:
  default: -1
  type: integer

단계 4: 벽지 설정

이를 위해서는 일부 JavaScript를 작성해야 합니다. 이 코드는 가능한 한 빠르고 독립적으로 실행되어야 하므로, 초기화 파일 javascripts/discourse/initializer/your-theme-component.js를 생성합니다.

기본적인 부분부터 시작해 보겠습니다:

import { setOwner } from '@ember/owner';
import { withPluginApi } from 'discourse/lib/plugin-api';

const PLUGIN_ID = 'your-theme-component';

class YourThemeWallpaper {
  api;
  styleRoot;

  constructor(owner, api) {
    setOwner(this, owner);
    this.api = api;
    // `html` 요소 가져오기
    this.styleRoot = document.body.parentElement.style;
    this.loadWallpaperSettings();
  }

  // 나중에 더 많은 로직이 추가될 것입니다
}

export default {
  name: PLUGIN_ID,
  initialize(owner) {
    withPluginApi('1.34.0', (api) => {
      this.instance = new YourThemeWallpaper(owner, api);
    });
  }
}

이것은 테마 컴포넌트 JavaScript를 추가하기 위한 대량의 보일러플레이트 코드입니다. styleRoot 변수는 문서의 html 요소를 참조해야 하며, 이를 통해 유효한 CSS를 변경할 것입니다.

loadWallpaperSettings 함수는 사용자 설정을 실제로 로드하는 역할을 하며, 다음과 유사한 형태를 가집니다:

loadWallpaperSettings() {
  let user = this.api.getCurrentUser();
  if (user) {
    this.api.container.lookup('store:main').find('user', user.username).then((user) => {
      let wp = user.user_fields[settings.wallpaper_user_field] || '';
      this.setWallpaper(wp);
    });
  } else {
    this.setWallpaper('');
  }
}

사용자 필드 값을 가져오기 위해서는 추가적인 API 호출이 필요하며, 이는 잠시 시간이 걸릴 수 있습니다. 이 호출이 반환되면 사용자 필드 값을 가져와 해당 값으로 setWallpaper를 호출합니다. settings.wallpaper_user_field는 단계 #3의 테마 설정에 대한 참조입니다. 설정이 구성되지 않았다면 wp 필드는 빈 문자열로 기본값이 설정됩니다.

현재 사용자가 없다면 빈 문자열로 setWallpaper 함수를 호출하여 기본 벽지를 로드합니다.

setWallpaper 함수는 실제로 벽지를 업데이트합니다:

setWallpaper(wallpaper) {
  if (!wallpaper || wallpaper === '') {
    // 설정되지 않은 경우 기본값 사용
    wallpaper = 'default';
  }
  // CSS 변수에 맞도록 값을 정규화
  wallpaper = wallpaper.toLowerCase().replaceAll(/[^a-z0-9]+/g, '-');
  if (wallpaper === 'none') {
    this.styleRoot.setProperty('--wallpaper', 'none');
  } else {
    this.styleRoot.setProperty('--wallpaper', 'var(--wallpaper-'+wallpaper+')');
  }
}

구성된 벽지 설정이 "none"인 경우 배경 이미지를 제거합니다. 그렇지 않으면 값이 CSS 변수를 닮도록 정규화됩니다. 예를 들어 "Alternative One"은 CSS 변수 --wallpaper를 var(--wallpaper-alternative-one)으로 설정하는 결과를 낳습니다. 사용자 필드 값이 CSS 변수에 매핑되지 않으면 사실상 벽지가 없는 상태가 되며, 이는 충분히 수용 가능합니다.

단계 5: 더 빠른 로드

위 설정은 잘 작동합니다. 그러나 추가적인 API 호출로 인해 벽지가 상당히 늦게 설정될 수 있습니다. 이는 좋은 결과를 제공하지 않습니다. 이를 해결하기 위해 브라우저의 Local Storage를 사용하여 설정을 저장하고 검색합니다.

생성자(constructor)에서 사용자의 벽지 설정을 로드하기 전에, 먼저 로컬 스토리지의 데이터를 기반으로 벽지를 설정해 보도록 합니다. 사용자 설정을 로드할 때, 로컬 스토리지에 구성된 내용을 업로드합니다.

const WALLPAPER_KEY = 'your_theme_wallpaper';

constructor(owner, api) {
  setOwner(this, owner);
  this.api = api;
  // `html` 요소 가져오기
  this.styleRoot = document.body.parentElement.style;
  this.loadLocalStorage();
  this.loadWallpaperSettings();
}

loadWallpaperSettings() {
  let user = this.api.getCurrentUser();
  if (user) {
    this.api.container.lookup('store:main').find('user', user.username).then((user) => {
      let wp = user.user_fields[settings.wallpaper_user_field] || '';
      localStorage.setItem(WALLPAPER_KEY, wp);
      this.setWallpaper(wp);
    });
  } else {
    localStorage.removeItem(WALLPAPER_KEY);
    this.setWallpaper('');
  }
}

loadLocalStorage() {
  let data = localStorage.getItem(WALLPAPER_KEY);
  if (data) {
    this.setWallpaper(data);
  }
}

이것은 Discourse 로드 초기에 벽지를 설정하는 것을 크게 개선합니다. 사실상 setWallpaper 함수가 두 번 호출됩니다. 하지만 값이 거의 변하지 않으므로 이는 눈에 띄지 않습니다. 사용자가 벽지를 변경한 경우에만 다음 로드 시 플리커가 발생할 수 있습니다. 이것이 CSS에서 초기 벽지를 기본값 대신 none으로 설정한 이유이기도 합니다.

단계 6: 실시간 미리보기

Discourse에서 색상 스킴을 변경할 때, 저장하고 새로고침할 필요 없이 새로운 색상 스킴의 실시간 미리보기를 볼 수 있습니다. 벽지 설정에도 이것이 가능하면 사용자가 어떤 것을 가장 좋아하는지 확인할 수 있어 매우 좋습니다.

이를 위해 사용자 필드 컴포넌트를 수정하여 몇 가지 추가적인 트릭이 필요합니다.

생성자의 끝 부분에 this.setupLivePreview() 호출을 추가했으며, 그 안에는 다음이 포함되어 있습니다:

setupLivePreview() {
  api.onPageChange((url, title) => {
    if (this.wallpaperPreviewed) {
      this.wallpaperPreviewed = false;
      this.loadWallpaperSettings();
    }
  });

  let _this = this;
  api.modifyClass('component:user-fields/dropdown', {
    pluginId: PLUGIN_ID,
    didUpdateAttrs() {
      if (this.field.id == settings.wallpaper_user_field) {
        _this.wallpaperPreviewed = true;
        _this.setWallpaper(this.value);
      }
    }
  });
}

첫 번째 부분에서는 페이지 변경 이벤트를 청취합니다. wallpaperPreviewed가 true였다면, 사용자가 원래 구성한 설정을 다시 로드합니다. 사용자가 사용자 필드의 새 값을 저장하지 않았다면, 벽지는 원래 설정으로 복원됩니다.

다음은 진정한 마법입니다. 드롭다운 사용자 필드 컴포넌트를 수정하여 didUpdateAttrs 함수에 추가 코드를 추가합니다. 이는 사용자 필드가 새 값을 얻을 때 호출됩니다. 이때 wallpaperPreviewed 변수를 true로 설정하여 페이지 변경 시 저장된 설정이 로드되도록 합니다. 다음으로, 현재 선택된 벽지를 표시하기 위해 setWallpaper 함수를 호출합니다.

다른 사용자 필드 유형에 대해서도 동일한 작업을 수행할 수 있습니다. 예를 들어, 일반 텍스트 필드의 경우 component:user-fields/text 클래스를 수정하면 됩니다.

결론

이 방법을 통해 사용자 필드를 사용하여 테마 컴포넌트가 사용자의 경험에 영향을 미치는 방식을 사용자가 제어할 수 있습니다.

유일한 문제는 사용자 필드가 환경설정 > 프로필 아래에 위치해 있다는 것이며, 환경설정 > 인터페이스 아래에 있는 것이 더 적합할 것입니다.

10개의 좋아요