새로운 Modal을 구현하고 있다면, 여기의 주요 문서를 확인해 보세요. 이 주제는 기존 컨트롤러 기반 Modal을 새로운 컴포넌트 기반 API로 마이그레이션하는 방법에 대해 설명합니다.
과거에 Discourse는 모달을 렌더링하기 위해 Ember-컨트롤러 기반 API를 사용했습니다. 모달을 호출하려면 컨트롤러 이름을 문자열로 showModal()에 전달해야 했습니다. 내부적으로 이는 Ember의 Route#renderTemplate API를 사용했는데, 이 API는 Ember 3.x에서 비추천(deprecated) 상태이며 Ember 4.x에서 제거될 예정입니다.
Discourse가 Ember 4.x 이상으로 업그레이드할 수 있도록, 모달을 위한 새로운 컴포넌트 기반 API를 도입했습니다. 이 새로운 API는 Ember의 ‘선언적’ 설계 패턴을 수용하며, 깔끔한 DDAU(Data Down, Actions Up) 의미를 제공하도록 설계되었습니다.
단계 1: 파일 이동
컨트롤러 JS 파일과 템플릿 파일을 /components/modal 디렉터리로 이동하세요. 이렇게 하면 '동반(colocated) 컴포넌트’가 되어 다른 JS 모듈과 마찬가지로 가져올 수 있습니다.
단계 2: JS 파일 업데이트
이제 컴포넌트 JS 정의를 @ember/controller가 아닌 @ember/component을 상속하도록 업데이트하세요 [1]. ModalFunctionality 믹신을 제거하고, 아래 표에 따라 해당 함수 사용법을 업데이트하세요:
| 이전 | 이후 |
|---|---|
flash() 및 clearFlash() |
컴포넌트에 flash 속성을 생성하고 <DModal>의 @flash 인자로 전달하세요. 기본적으로 알림은 alert 클래스로 스타일링되며 이는 ‘error’ 클래스의 복사본이지만, @flashType 인자를 사용하여 오버라이드할 수 있습니다. |
showModal() |
discourse/lib/show-modal에서 showModal 함수를 가져오세요 |
closeModal 액션 |
컴포넌트로 자동 전달되는 closeModal 인자를 호출하세요 |
구식 스타일의 모달 컨트롤러는 ‘영구적으로’ 존재했기 때문에 상태를 수동으로 정리해야 했습니다. 새로운 컴포넌트 기반 API에서는 모달이 표시/숨겨질 때 컴포넌트가 생성되고 파괴됩니다. 많은 경우 기존 라이프사이클 훅이 더 이상 필요하지 않음을 의미합니다.
여전히 라이프사이클 기반 로직이 필요하다면 다음 표를 사용하세요:
| 이전 | 이후 |
|---|---|
onShow() |
표준 Ember 컴포넌트 라이프사이클(init() 또는 Ember modifier) 사용 |
afterRender |
표준 Ember 컴포넌트 라이프사이클(init() 또는 Ember modifier) 사용 |
beforeClose() |
컴포넌트로 전달되는 @closeModal 인자를 감싸는 래퍼를 생성하세요. 클로즈 래퍼의 참조를 DModal에 전달하여 <DModal @closeModal={{this.myCloseModalWrapper}}처럼 사용하세요 |
onClose() |
표준 Ember 컴포넌트 라이프사이클(willDestroy() 또는 Ember modifier) 사용 |
단계 3: 템플릿 업데이트
<DModalBody> 래퍼를 <DModal>로 교체하세요. 몇 가지 새로운 속성을 추가합니다:
- 새로운
@closeModal인자를 전달하세요. - 명시적인 클래스를 추가하세요. 기존 동작과 일치시키기 위해 컨트롤러 파일명에
-modal을 추가하세요.
예를 들어, 모달 컨트롤러가 close-topic.js였다면, 새로운 <DModal> 호출은 다음과 유사해 보일 것입니다:
<DModal @closeModal={{@closeModal}} class="close-topic-modal">
DModalBody 호출에 다른 인자가 포함된 경우, 아래 표에 따라 업데이트하세요:
| Before | After |
|---|---|
@title="title_key" |
@title={{i18n "title_key"}} |
@rawTitle="translated title" |
@title="translated title" |
@subtitle="subtitle_key" |
@subtitle={{i18n "subtitle_key"}} |
@rawSubtitle="translated subtitle" |
@subtitle="translated subtitle" |
@class |
@bodyClass |
@modalClass |
일반 html 속성을 가진 앵글 브래킷 문법 사용: <DModal class="blah"> |
@titleAriaElementId |
일반 html 속성을 가진 앵글 브래킷 문법 사용: <DModal aria-labelledby="blah"> |
@dismissable, @submitOnEnter, @headerClass |
변경 없음 |
기존 <DModalBody> 컴포넌트 뒤에 렌더링되던 푸터 콘텐츠가 있었다면, <DModal> 내부에 이를 도입하기 위해 새로운 <:footer> 이름 지정 블록(named block)을 사용하세요. 이름 지정 블록을 사용할 때, 본문 콘텐츠는 <:body></:body>로 감싸야 합니다. 예를 들어:
<DModal @closeModal={{@closeModal}}>
<:body>
Hello world, this is the content of the modal
</:body>
<:footer>
This is the footer content. A `.modal-footer` wrapper will be added
automatically
</:footer>
</DModal>
단계 4: showModal 호출 지점 업데이트
이전에는 모달이 showModal API를 사용하여 렌더링되었는데, 이는 문자열(컨트롤러 이름)과 여러 옵션을 취했습니다. 컨트롤러 인스턴스를 반환하여 조작할 수 있었습니다:
import showModal from "discourse/lib/show-modal";
export default class extends Component {
showMyModal() {
const controller = showModal("my-modal", {
title: "My Modal Title",
modalClass: "my-modal-class",
model: { topic: this.topic },
});
controller.set("updateTopic", this.updateTopic);
});
}
새로운 컴포넌트 기반 모달을 렌더링하려면 ‘modal’ 서비스를 주입하거나(getOwner(this).lookup("service:modal")와 같은 방식으로 액세스) show() 함수를 호출해야 합니다.
show()는 첫 번째 인자로 새로운 컴포넌트 클래스의 참조를 취합니다. 여전히 지원되는 유일한 옵션은 'model’로, 모달에 필요한 모든 데이터/액션을 전달하는 데 사용할 수 있습니다.
컴포넌트 인스턴스에 대한 참조는 반환되지 않습니다. 대신 show()는 모달이 닫힐 때 이행(resolve)되는 프롬ises를 반환합니다. 이 프롬ises는 @closeModal에 전달된 데이터와 함께 이행됩니다.
import MyModal from "discourse/components/my-modal";
import { service } from "@ember/service";
export default class extends Component {
@service modal;
showMyModal() {
this.modal.show(MyModal, {
model: { topic: this.topic, updateTopic: this.updateTopic },
});
});
}
대안으로, 주요 DModal 문서에 설명된 선언적 API로 마이그레이션할 수 있습니다.
기존 옵션의 기능은 다음과 같이 재현할 수 있습니다:
Old showModal opt |
Solution |
|---|---|
admin |
컴포넌트에 해당 없음 - 제거하세요 |
templateName |
컴포넌트에 해당 없음 - 제거하세요 |
title |
<DModal @title={{i18n "blah"}}>로 이동 |
titleTranslated |
<DModal @title="blah">로 이동. 필요할 경우 model의 데이터 기반으로 계산할 수 있습니다 |
modalClass |
<DModal class="blah">로 이동 |
titleAriaElementId |
<DModal aria-labelledby="blah">로 이동 |
panels |
컴포넌트에서 탭을 구현하기 위해 <:headerBelowTitle> 이름 지정 블록 사용 (예시) |
model |
변경 없음 |
단계 5: 테스트
테스트는 대부분 동일하게 유지되어야 합니다. 가장 흔한 문제는 다음과 같습니다:
-
모달은 더 이상 이름 기반의 기본 클래스를 가지지 않습니다. 클래스는 템플릿에서 명시적으로 지정해야 합니다(단계 3의 시작 부분 참조)
-
모달이 닫힐 때
d-modal래퍼가 더 이상 DOM에 유지되지 않습니다. 모든 모달이 닫혔는지 확인하려면assert.dom('.d-modal').doesNotExist()와 같은 체크를 사용하세요
성공!
이제 모달이 이전과 동일하게 작동해야 합니다. 새로운 API의 이점을 더 많이 활용하려면, 선언적 전략으로 showModal 호출을 교체하고 모달을 Glimmer 컴포넌트로 변환하는 것을 고려해 보세요.
예시
Discourse 코어의 일부 모달을 새로운 API로 변환하는 과정을 보여주는 예시 커밋입니다:
이 문서는 버전 관리됩니다 - github에서 변경 사항을 제안하세요.
이 가이드에서는 Ember 컨트롤러에서 가장 쉬운 마이그레이션 경로를 제공했기 때문에 클래식 Ember 컴포넌트를 권장합니다. 하지만 간단한 모달이거나 리팩토링에 시간을 들이는 것이 괜찮다면, 모던 Glimmer 컴포넌트가 더 나은 선택입니다. ↩︎