Glimmer 컴포넌트 변경 감지가 작동하지 않음 - @tracked 배열 속성 변경

여러분 안녕하세요!

Glimmer 컴포넌트에서 변경 감지(change detection) 관련으로 문제가 발생해서 조언이 필요합니다. @tracked 배열 내 객체의 프로퍼티를 변경(mutation)해도 템플릿이 다시 렌더링되지 않습니다.

**현재 설정:**

```javascript

import Component from '@glimmer/component';

import { tracked } from '@glimmer/tracking';

import { fn } from '@ember/helper';

import { on } from '@ember/modifier';

import { action } from '@ember/object';



export default class CustomSidebarComponent extends Component {

  @tracked items = [

    {

      id: 'home',

      label: 'Home',

      expanded: false

    },

    {

      id: 'my-posts', 

      label: 'My Posts',

      expanded: false

    }

    // ... 더 많은 항목

  ];



  @action

  toggleExpanded(item) {

    item.expanded = !item.expanded; // 이 변경은 다시 렌더링을 트리거하지 않습니다

    console.log('Toggled:', item.label, item.expanded); // 정상적으로 로그가 출력됩니다

  }



  <template>

    <div id="custom-sidebar">

      <ul>

        {{#each this.items key="@index" as |item|}}

          <li class="menu-item {{if item.expanded 'expanded'}}" {{on "click" (fn this.toggleExpanded item)}}>

            {{item.label}} {{if item.expanded "(expanded)" ""}}

          </li>

        {{/each}}

      </ul>

    </div>

  </template>

}

```

**문제:**

1. 클릭 핸들러가 정상적으로 실행됩니다

2. item.expanded 프로퍼티가 업데이트됩니다 (콘솔에서 확인됨)

3. 하지만 템플릿이 다시 렌더링되지 않습니다 - 클래스 변경 없음, 텍스트 변경 없음

**시도해 본 것들:**

1. **배열 재할당** - 변경 후 this.items = [...this.items] 실행 (동작하지 않음)

2. **Immer를 이용한 불변 업데이트** - 시도하고 싶었지만 Discourse 테마에서 npm 임포트를 작동시킬 수 없었습니다

3. **다른 키 전략** - key="@index" 대신 key="id" 사용

**질문:**

- Glimmer에서 @tracked 배열 내 객체의 프로퍼티를 변경하는 것이 동작하도록 의도된 것인가요?

- 개별 객체를 어떤 방식으로든 tracked로 만들어야 하나요?

- 이 사용 사례에 대한 권장 패턴이 있나요?

- 이것이 Discourse 테마 환경에서 실행되고 있기 때문일 수 있나요?

- 반응성 라이브러리에 영향을 미치는 Discourse 테마의 npm 임포트 제한 사항이 있나요?

**환경:**

- Discourse 테마 컴포넌트

- .gjs 파일이 있는 Glimmer 컴포넌트

- 최신 Discourse 버전 - 오늘 업데이트됨

어떤 통찰도 큰 도움이 될 것입니다! Glimmer의 반응성 시스템에 대해 제가 근본적으로 놓치고 있는 것이 있나요?

감사합니다!

음, 좀 이상하네. 배열 할당 트릭은 보통 잘 먹혀 주거든.

내가 찾은 대안은 배열 안의 객체를 자체적으로 추적되는 프로퍼티(tracked properties)를 가진 클래스로 만드는 거야. 이런 식으로 말이지:

class CustomSidebarItem {
  @tracked expanded = false;
  constructor(id, label) {
    this.id = id;
    this.label = label;
  }
}

export default class CustomSidebarComponent extends Component {

  @tracked items = [
    new CustomSidebarItem('home', 'Home'),
    new CustomSidebarItem('my-posts', 'My Posts'),
    ...
  ];
  // 나머지 코드
}

단순 객체를 여러 개 만드는 것보다 코드가 더 길어질 수 있지만, 확장하기가 더 쉽고 논리적으로 이해하기가 편하다는 걸 알게 됐어. 특히 데이터를 중첩된 컴포넌트로 내려 보내야 하는 경우가 있을 때 더 그렇지.

Hi Alteras,

제안해 주셔서 감사합니다!!! 제안하신 방법대로 진행했는데, 정말 잘 작동했고 하루를 좀 더 쉽게 만들어 주었습니다 ::)** :grinning_face:

OP에서 설명한 대로 배열을 추적하면, 배열 자체의 참조를 추적하는 것이지 배열 내 개별 객체의 변경 사항은 추적하지 않는다고 생각합니다.

다른 해결 방법으로는 trackedObject를 사용하는 것이 있습니다. 우리는 Discourse 전반에서 여러 곳에 이 방식을 사용하고 있습니다.

피드백 주셔서 감사합니다. 추가로 질문이 있습니다:

immer 같은 라이브러리를 사용하면 될까요?
가능하다면, discourse에 immer를 어떻게 포함해야 하나요?
'node_modules에서 assets로 복사한다’는 언급을 본 적이 있지만, npm을 통해 처리하거나, 헤더 태그에 CDN 링크를 사용하는 방식이 있다면 더 좋겠습니다.

loadScript()를 사용하면 외부 URL이더라도 JS를 로드할 수 있습니다. jsdelivr와 같은 외부 서비스를 사용하면 이 방식으로 작동해야 합니다.

import loadScript from "discourse/lib/load-script";

...

loadScript(URL).then(() => {
   // your code
})

광범위하게 테스트해 보지는 않았지만, 동적 import도 작동할 것입니다.

async function load() {
  const {
    default: myDefault,
    foo,
    bar,
  } = await import(URL);
  // rest of code
}

참고로, 해당 JS의 중요도에 따라 외부 CDN에 의존하기보다는 NPM 파일을 코드로 직접 저장하는 것이 더 안전할 수 있습니다. 마찬가지로, 브라우저에서 사용하기 전에 번들링 및 전처리 작업을 조금 수행해야 할 수도 있습니다.


Immer가 Discourse와 함께 작동하는지에 대해서는 제가 알지 못합니다.