Discourse는 사용자가 게시물(그리고 다른 곳)에 이미지를 업로드할 수 있도록 허용합니다. 때로는 이미지가 표시하기에 너무 크므로, Discourse는 이미지의 축소된 버전을 생성하여 게시물에 추가합니다. 이 축소된 이미지를 클릭하면 전체 크기의 이미지가 포함된 멋진 오버레이가 나타납니다. 이것은 일반적으로 라이트박싱(lightboxing)이라고 불립니다.
Discourse는 현재 라이트박싱 동작을 처리하기 위해 Magnific popup이라는 라이브러리를 사용합니다. 이 주제는 Magnific popup에서 사용자 및 개발자가 오늘날 기대하는 수준에 더 부합하는 것으로 마이그레이션하여 Discourse의 라이트박싱을 개편하는 것에 관한 것입니다.
이유
Magnific은 훌륭한 라이브러리이며, 프로젝트 페이지의 별점 수는 수년간 사용자와 개발자 모두에게 얼마나 많은 문제를 해결해 주었는지를 명확하게 보여줍니다.
또한 사용 편의성과 기능 제공 측면에서 시대를 앞섰습니다. 오늘날 볼 수 있는 거의 모든 기능이 2014년에 출시된 v 1.0에 이미 포함되어 있었다는 점을 고려하면 이것이 분명해집니다.
그렇다면 우리는 어디에 서 있는 걸까요? 간략히 하겠습니다. Magnific popup은 크로스 브라우저 호환성이 오늘날보다 훨씬 뒤처져 있던 완전히 다른 시대를 위해 만들어졌습니다. 이는 네 가지를 의미합니다.
- jQuery를 의존성으로 가지고 있습니다.
- 고대 브라우저용 코드가 포함되어 있습니다.
- 웹에 액세스하는 데 사용되는 평균 기기가 그때 이후로 많이 변했습니다.
- Discourse와 같은 단일 페이지 애플리케이션(SPA)이 아닌, 즉 정적 페이지를 우선시하도록 설계되었습니다. 이는 단일 페이지 애플리케이션에 특정한 성능 문제를 일으키며, 나중에 자세히 다루겠습니다.
현재 웹 표준의 상태와 바닐라 JavaScript로 얼마나 많은 마법을 부릴 수 있는지를 고려하면, jQuery를 의존성으로 유지하는 것에서 벗어나고 있는 Discourse와 같은 대규모 프로젝트의 움직임은 이해할 수 있습니다. 참고로 jQuery에 대한 논의는 여기서 오프톱픽이며, 이는 주로 맥락 설명을 위한 것입니다.
즉, 이유는… jQuery 사용 감소, 고대 브라우저 코드 제거, 전반적인 성능 향상, 그리고 이제 매우 달라진 평균 기기에 대한 더 나은 지원입니다.
방법
제안되는 솔루션은 부족하지 않으며, 바퀴를 다시 발명하지 말아야 한다는 논의가 있을 수 있지만… 거기에 대해 이야기하지 않고 간략히 하겠습니다.
아마도 이 쟁점을 가장 짧게 다루는 방법은… 어떤 오프더셸(off-the-shelf) 솔루션이 아무리 잘 맞더라도… 맞춤 제작된 것만큼 잘 맞을 수는 없다는 것입니다. 여기까지 하겠습니다.
여기 고려할 것이 꽤 많습니다. 이미지, iframe, 비디오 등 다양한 사용 사례를 다루는 라이브러리에서 기능 과잉이 있거나, 라이선스에 대한 명확성이 부족할 수 있습니다.
Discourse가 지원해야 하는 특정 사용 사례를 알면 불필요한 코드를 피할 수 있으며, 많은 오버헤드 없이 추가적인 타겟팅된 기능을 추가할 수 있습니다.
따라서 이제 꽤 좋은 기준점을 갖게 되었습니다. 요구 사항은 다음과 같습니다.
- jQuery 없음
- 당분간 이미지에만 초점 맞춤
- 모바일에서 스와이프와 같은 품질 개선 지원
- 현재 테마/플러그인 시스템을 사용하여 추가적인 커스터마이징/개선을 허용하기 위해 Discourse와 통합.
내용
Discourse는 Ember 기반이므로, 새로운 라이트박스는 마크업과 데이터를 처리하기 위해 Ember 컴포넌트여야 합니다. 또한 앱의 어디에나 라이트박스를 설정할 수 있어야 한다는 기대가 있으므로, 라이트박스 서비스를 가지는 것이 매우 합리적입니다. 이를 통해 개발자는 다음을 수행할 수 있습니다.
- 서비스를 어떤 컴포넌트에든 주입하여 서비스의 설정/청소 메서드를 컴포넌트 라이프사이클에 연결할 수 있습니다.
- 앱의 어디에서든 서비스를 조회하여 설정/청소 메서드를 호출할 수 있습니다.
추가로, 품질 개선을 위해 추상화를 조금 두어 테마에서 사용할 수 있는 유틸리티 함수를 몇 개 만들 수 있습니다. 나중에 더 자세히 다룰 것입니다.
목표 중 하나가 테마/플러그인이 기능을 확장할 수 있도록 하는 것이므로, 라이트박스 서비스는 appEvents를 통해 상태 변경을 통신할 것입니다. 다음 이벤트를 방출합니다.
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
각 이벤트는 개발자가 일반적으로 기대하는 데이터 유형과 함께 트리거됩니다. 예를 들어, lightbox:opened 이벤트에는 모든 항목 목록과 라이트박스가 열린 현재 항목이 포함됩니다. 나중에 더 자세히 다룰 것입니다.
그것을 정리했으니, 계속하겠습니다.
지난 몇 주 동안 저는 Discourse Lightbox를 소개하는 초안 PR을 작업해 왔습니다.
한눈에 보면 다소 클 수 있으므로, 이를 분해해 보겠습니다.
많은 사용자 상호작용에 의존하는 것을 테스트하는 것은 까다롭기 때문에, 라이트박스 테스트 스위트에는 63개의 테스트와 291개의 어설션이 포함되어 있습니다.
테스트 크기를 정리했으니, Magnific popup(둘 다 미압축 상태)과 간단한 크기 비교를 해 보겠습니다.
| Discourse Lightbox LOC | Magnific Popup LOC | 차이 | |
|---|---|---|---|
| Javascript | 1197 | 1860 | (35%) |
| CSS | 813 | 351 | 131% |
| Templates | 401 | 0 | - |
| Total | 2411 | 2211 | 9.1% |
따라서 Discourse Lightbox는 Magnific보다 약 9% 더 많은 코드를 가지고 있습니다. 물론, 이는 두 가지 이유로 이야기의 일부일 뿐입니다.
- Magnific이 의존하는 jQuery를 고려하지 않았습니다. jQuery 3.6은 대략 11k LOC입니다.
- Discourse Lightbox는 Magnific보다 더 많은 기능을 추가합니다.
그 기능들을 살펴볼까요,
아래 비디오에서 보이는 잔상은 녹화와 관련이 있으며, 사용자가 경험할 것이 아닙니다. 모든 애니메이션/전환은 안정적인 60FPS로 실행됩니다.
그러면, 더 이상의 지연 없이,
기본 레이아웃
비교를 위해, 현재 Magnific 구현의 동일한 레이아웃을 보여 드리겠습니다
위 비디오에 대한 몇 가지 포인트
-
새로운 라이트박스는 일반적인 반투명 어두운 배경 대신 이미지를 배경으로 사용할 것입니다. 이미지는 정의상 스스로를 보완합니다. 참고로 이는 네트워크 오버헤드를 추가하지 않습니다. 배경으로 사용되는 이미지는 게시물 본문에서 가져온 것이므로, 사용자가 라이트박스를 열 때 이미 캐시되어 있을 것입니다.
-
Discourse Lightbox는 고정된 UI를 선택합니다. 닫기 버튼, 제목, 이미지 메타데이터가 이미지에 부착되어 있는 대신, 자체적으로 예약된 공간을 가집니다. 이는 서로 다른 치수의 이미지 사이를 탐색할 때 점프를 줄이는 데 도움이 될 것입니다.
-
이미지 간 탐색은 라이트박스의 화살표나 키보드 단축키로 수행할 수 있습니다. 다음을 위해 → 또는 ↓, 이전을 위해 ← 또는 ↑. RTL 로케일에서는 단축키가 이에 따라 반전됩니다.
모바일 레이아웃은 매우 유사하지만, 탐색을 위한 스와이프 제스처가 추가되므로 화살표가 표시되지 않습니다.
모바일에서 왼쪽으로 스와이프하면 다음으로, 오른쪽으로 스와이프하면 이전으로 이동합니다. RTL 로케일에서는 스와이프 제스처가 반전됩니다.
줌
새로운 라이트박스에는 보고 있는 이미지가 뷰포트 치수보다 클 때 보이는 전용 줌 버튼이 있습니다. 줌 버튼을 클릭하면 이미지가 확대되고, 버튼을 다시 클릭하면 축소됩니다. 추가로, 이제 확대/축소를 위한 키보드 단축키 Z가 있습니다. 마지막으로, 이미지가 줌 가능하면 클릭해도 동일한 효과가 있습니다.
세 가지 방법을 모두 보여주는 비디오입니다
위 비디오에서 시연하지는 않았지만, 줌할 수 있는 이미지에 마우스를 올리면 커서가 이를 반영하도록 변경됩니다. 이미 확대된 이미지에 마우스를 올리면 zoom-out 아이콘으로 변경됩니다.
새로운 라이트박스에서 줌은 다르게 작동합니다. 데스크톱에서, 이미지의 확대된 섹션은 커서를 따라갑니다.
모바일에는 호버가 없으며, 확대된 이미지 안에서 이동하려면 일반 터치 스크롤을 사용합니다.
회전
새로운 라이트박스는 이미지를 90도 단위로 회전하기 위한 전용 회전 버튼을 추가합니다. 회전에도 키보드 단축키가 있습니다. R 키입니다. 다음과 같이 보입니다.
회전과 줌을 결합할 수 있습니다.
전체 화면 모드
새로운 라이트박스는 전체 화면 버튼을 추가합니다. 이 버튼은 브라우저 창을 전체 화면 모드로 진입시킵니다. 키보드 단축키는 M입니다.
상태를 추적하고, 전체 화면이 꺼지거나 라이트박스가 닫힐 때 일반 모드로 돌아갑니다.
다운로드
현재 라이트박스는 이미지 아래에 “다운로드” 링크를 추가합니다. 새로운 라이트박스는 동일하게 하지만, 이를 아이콘으로 변경하여 라이트박스의 푸터에 추가합니다. 여전히 동일한 권한을 준수합니다. 만약…
prevent_anons_from_downloading_images
이 활성화되어 있고 사용자가 로그인되어 있지 않으면, 다운로드 아이콘은 표시되지 않습니다.
새 탭
현재 라이트박스는 이미지를 새 탭에서 여는 “원본” 링크를 이미지 아래에 추가합니다. 새로운 라이트박스는 동일하게 하지만, 이를 라이트박스 헤더에 아이콘으로 추가합니다. 다운로드 아이콘에 대한 동일한 권한 집합도 준수합니다.
이미지 제목
새로운 라이트박스는 주로 이미지에 초점을 맞춥니다. 이미지 제목은 기본적으로 한 줄로 잘리지만, 확장 지원을 합니다. 다음과 같이 보입니다.
제목의 확장/축소를 위한 키보드 단축키 T가 있으며, 이미지가 확대/회전된 경우 제목은 표시되지 않습니다.
캐러셀
새로 사용 가능한 기능은 갤러리의 모든 이미지를 캐러셀로 표시하는 것입니다. 레이아웃은 기기의 화면에 따라 달라지며, 수평 또는 수직일 수 있고, 다음과 같이 보입니다.
캐러셀을 전환하는 키보드 단축키가 있습니다. A
그리고 모바일에서는 다음과 같이 보입니다.
모바일에서 아래로 스와이프하면 캐러셀이 켜지고 꺼집니다.
닫기
Esc는 데스크톱에서 이전과 마찬가지로 라이트박스를 닫기 위해 작동합니다. 이제 모바일에 추가적인 스와이프 제스처가 있으며, 위로 스와이프하여 라이트박스를 닫을 수 있습니다.
접근성
버튼 레이블과 같은 기본 사항 외에도, 새로운 라이트박스는 화면 밖의 스크린 리더 announcer 요소를 추가합니다. 라이트박스 내에서 이미지로 탐색하면, 다음 형식에 따라 인덱스와 제목을 읽어 줍니다.
image %{current} of %{total}: %{title}
짧은 예시는 다음과 같습니다.
새로운 라이트박스는 또한 aria-hidden을 통해 스크린 리더에서 목적을 수행하지 않는 불필요한 모든 버튼을 제거합니다.
접근성은 지속적인 과제이며, 이는 결코 완료된 것이 아닙니다. 항상 개선할 것이 있지만, v1에서 단순성을 유지하기 위해 여기서 멈췄습니다.
이것으로 새로운 라이트박스의 모든 기능을 다뤘습니다.
개발자 용어로 조금 이야기해 보겠습니다.
이벤트 리스너
현재 라이트박스는 cooked 게시물에 있는 각 개별 라이트박스 이미지에 클릭 이벤트 리스너를 추가합니다. 즉, 20개의 이미지가 있는 게시물은 라이트박스를 위해 20개의 클릭 이벤트 리스너를 가집니다.
새로운 라이트박스는 이벤트 위임을 활용하여 게시물 자체에 하나의 이벤트 리스너만 추가합니다.
20개의 이미지가 있는 게시물에서 가비지 컬렉션을 강제 한 후 현재 라이트박스의 익명 사용자, 인코그니토 윈도우 이벤트 리스너 수는 다음과 같습니다.
그리고 새로운 라이트박스를 적용한 동일한 게시물은 다음과 같습니다.
추가로, 현재 라이트박스 내에서 탐색하면 이벤트 리스너가 추가되며, 이는 고아가 되어 가비지 컬렉션에 간섭합니다. 차트는 다음과 같습니다.
- 20개의 이미지를 포함하는 하나의 게시물이 있는 토픽 페이지 로드.
- 라이트박스를 열고 20개의 이미지를 3회 연속으로 탐색.
- 라이트박스 닫기
- 브라우저 가비지 컬렉션 강제
현재 라이트박스:
새로운 라이트박스:
라이트박스 자체의 이벤트 리스너에 관해서는, 현재 Magnific에서 정리되지 않는 것 같습니다(기억하세요, 단일 페이지 애플리케이션을 위해 만들어지지 않았습니다).
위 테스트에 대한 간단한 참고: 매우 기본적인 것이며, 숫자는 "과학적"이려는 것이 아닙니다. 여기서의 목표는 정확한 숫자가 아니라 방향을 파악하는 것입니다.
개발자 노트
새로운 Discourse Lightbox로 라이트박스를 설정하고 정리하는 방법에 대해 이야기해 보겠습니다.
라이트박스 설정 및 정리
개발자에게는 두 가지 옵션이 있습니다.
-
라이트박스 서비스를 컴포넌트에 주입
import { inject as service } from "@ember/service"; //... @service lightbox그런 다음 호출할 수 있습니다.
this.lightbox.setupLightboxes({ container: yourContainer // DOM 노드 selector: ".css-selector" // 라이트박싱하려는 요소의 문자열 선택자 })그리고 정리하고 싶을 때, 그냥 호출하세요.
this.lightbox.cleanupLightboxes()그게 전부입니다.
-
라이트박스 서비스를 주입하지 않으려면,
setupLightboxes와cleanupLightboxes를 다음과 같이 가져올 수 있습니다.import { cleanupLightboxes, setupLightboxes, } from "discourse/lib/lightbox";나머지는 서비스를 주입하는 것과 동일합니다. 그 두 함수가 서비스를 조회해 줍니다. 따라서
setupLightboxes({ container: yourContainer // DOM 노드 selector: ".css-selector" // 라이트박싱하려는 요소의 문자열 선택자 }) //.... cleanupLightboxes()
호출을 서비스에서 직접 하든 유틸리티 함수를 통해 하든, 하위 호환성을 위해 nodeList도 허용하지만 권장하지 않습니다.
이 부분에 대한 마지막 참고는, 설정한 라이트박스를 여는 트리거로 동작하는 비이미지 요소도 가질 수 있다는 것입니다. 예를 들어
<div class="my-container">
<img class="my-selector" src="foo">
<img class="my-selector" src="bar">
....
<button>Open Lightbox</button>
</div>
위 div에 기본 라이트박스를 설정하려면 다음과 같이 하겠습니다.
import {
cleanupLightboxes,
setupLightboxes,
} from "discourse/lib/lightbox";
//...
setupLightboxes({
container: document.querySelector(".my-container"),
selector: ".my-selector"
})
버튼이 라이트박스를 열려면, data-lightbox-trigger를 다음과 같이 추가하기만 하면 됩니다.
<button data-lightbox-trigger>Open Lightbox</button>
나머지는 자동으로 처리됩니다.
마지막으로, 정리하고 싶을 때 호출하세요.
cleanupLightboxes()
정리는 정말로 중요하지 않습니다. 왜냐하면 라이트박스 서비스는 앱에서 dom:clean 이벤트가 발생하면(라우트 전환 시) 자동으로 정리하기 때문입니다.
라이트박스 이벤트 청취
새로운 라이트박스는 이전에 논의했듯이 이벤트를 방출합니다. 그 이벤트들은 다음과 같습니다:
lightbox:openedlightbox:item-will-changelightbox:item-did-changelightbox:closed
lightbox:opened
이 이벤트는 라이트박스가 열릴 때 방출되며 두 개 객체를 포함합니다.
items: 현재 라이트박스의 모든 이미지의 배열입니다. 각 항목은 객체입니다.currentItem: 라이트박스가 열린 현재 항목의 객체입니다.
항목 객체는 다음과 같습니다:
{
"fullsizeURL": "https://global.discourse-cdn.com/meta/original/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff.jpeg",
"smallURL": "https://global.discourse-cdn.com/meta/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg",
"downloadURL": "/uploads/short-url/56rKTvkvmL6c2C8OFQtMo3D8sIn.jpeg?dl=1",
"title": "Close-up Photo of a Black Microphone on Stand",
"fileDetails": "1200×1500 88.9 KB",
"dominantColor": "793C6D",
"aspectRatio": "400 / 500",
"index": 0,
"cssVars": "--dominant-color: #793C6D;--aspect-ratio: 400 / 500;--small-url: url(https://global.discourse-cdn.com/meta/optimized/4X/2/3/c/23c5746c48803deac5c105081dba20555187c3ff_2_600x750.jpeg);",
"isLoaded": true,
"hasLoadingError": false,
"width": 1200,
"height": 1500,
"canZoom": true
}
lightbox:item-will-change
이 이벤트는 라이트박스의 현재 항목이 변경되기 바로 직전에 방출됩니다. currentItem(변경될 항목)을 가집니다.
lightbox:item-did-change
이 이벤트는 라이트박스의 항목이 변경되고 로딩이 완료된 직후에 방출되며, currentItem을 인수로 가집니다.
lightbox:closed
이 이벤트는 라이트박스가 닫힌 직후에 방출되며 인수가 없습니다.
위 이벤트로, 이론적인 테마 컴포넌트는 다음과 같이 라이트박스에 분석을 쉽게 추가할 수 있습니다.
api.onAppEvent('lightbox:opened', ({items, currentItem}) => {
console.log({items});
console.log({currentItem});
// 분석 코드 여기에
});
또는 다른 유사한 아이디어.
CSS
새로운 라이트박스는 HTML 클래스에 BEM 네이밍 컨벤션을 사용합니다. 사용할 수 있는 선택자의 완전한 목록은 다음과 같습니다.
html.has-lightbox {
// 라이트박스가 열려 있을 때 HTML 요소용 css
}
.d-lightbox {
&--is-visible {
// 메인 라이트박스 요소
}
&__content {
// 라이트박스 내부 콘텐츠 래퍼
}
}
.d-lightbox {
&__content__header {
// 라이트박스 헤더
}
}
.d-lightbox {
&__content__body {
// 라이트박스 본문 (메인 이미지를 포함)
&__backdrop {
// 라이트박스 배경
}
&__main-image {
// 라이트박스 메인 이미지
}
&__error-message {
// 라이트박스 오류 메시지
}
&__previous-button,
&__next-button {
// 메인 라이트박스 이전/다음 버튼
}
}
}
.d-lightbox {
&__content__footer {
// 라이트박스 푸터
&__main-title {
// 라이트박스 이미지 제목
&__item-file-details {
// 라이트박스 파일 세부 정보 예: "1000x582 183KB"
}
}
}
}
.d-lightbox {
&__content__carousel {
// 라이트박스 캐러셀 컨테이너
&__previous-button,
&__next-button {
// 라이트박스 캐러셀 이전/다음 버튼
}
}
}
.d-lightbox {
&__content__carousel {
&__carousel-items {
// 라이트박스 캐러셀 항목 컨테이너
&__item,
&__item--is-current {
// 라이트박스 캐러셀 항목
}
&__item--is-current {
// 라이트박스 캐러셀 현재 항목
}
}
}
}
.d-lightbox {
&--is-vertical &__content__carousel {
// 라이트박스 캐러셀 수직 스타일
}
}
.d-lightbox {
&--is-horizontal &__content__carousel {
// 라이트박스 캐러셀 수평 스타일
}
}
.d-lightbox {
.btn-flat {
// 모든 라이트박스 버튼 스타일
}
}
.d-lightbox {
&__content {
&__focus-trap,
&__screen-reader-announcer {
// 라이트박스 포커스 트랩 및 스크린 리더 announcer. 이들은 화면 밖입니다.
}
}
}
/* 상태 스타일 */
// 캐러셀
.d-lightbox {
&--has-carousel {
// 캐러셀이 열려 있을 때 라이트박스 스타일
}
}
// 확장된 제목
.d-lightbox {
&--has-expanded-title {
// 제목이 확장되어 있을 때 라이트박스 스타일
}
}
// 줌
.d-lightbox {
&--can-zoom {
// 이미지를 줌할 수 있을 때 라이트박스 스타일
}
&--is-zoomed {
// 이미지가 줌되어 있을 때 라이트박스 스타일
}
}
// 회전
.d-lightbox {
&--is-rotated {
// 이미지가 회전되어 있을 때 라이트박스 스타일
}
}
// 전체 화면
.d-lightbox {
&--is-fullscreen {
// 이미지가 전체 화면일 때 라이트박스 스타일
}
}
내용을 다뤘으니, 이제 마침내 다음으로 넘어갈 수 있습니다…
시기
지금, PR은 리뷰를 준비되었습니다. 리뷰가 먼저 이루어져야 합니다. 리뷰를 통과하면 업데이트하는 사이트에서 사용할 수 있게 됩니다. PR은 Magnific popup에서 Discourse Lightbox로 전환되는 동안 새로운 임시 사이트 설정을 추가합니다. 설정의 이름은 다음과 같습니다:
enable_experimental_lightbox
설정이 비활성화되어 있으면, PR은 효과가 없으며 모든 것이 이전과 동일하게 Magnific popup과 함께 작동합니다.
Discourse Lightbox는 설정이 켜지면 cooked 게시물, 채팅 메시지, 이미지 업로더 컴포넌트에서 Magnific popup을 대체합니다.
로드맵
- PR 리뷰
- PR 병합
- 일반 피드백 기간 (1-2주)
- 코어에서 Magnific popup을 제거하고 실험적 사이트 설정을 제거하는 PR.
- TBD: 새로운 라이트박스가 레이아웃에 CSS 그리드를 사용하기 때문에 (직접적이어야 함) 다른 레이아웃을 탐색하는 테마 컴포넌트와 같은 스트레치 목표
감사의 글
- 이 작업은 CDCK의 관대한 후원으로 이루어졌습니다

- 시대를 훨씬 앞서 나간 것을 만들어 준 Magnific popup의 창시자 Dmytro Semenov에게 많은 사랑
을 보냅니다. - 위 데모에 사용된 이미지는 Irina Iriser @pexels의 제공으로 이루어졌습니다.




