특정 페이지에 콘텐츠를 추가하려면 최선의 선택은 플러그인 아웃렛(plugin-outlet)입니다. 간단히 말해, 플러그인 아웃렛은 Discourse 템플릿에 예약된 공간으로, 여기에 새로운 콘텐츠를 추가할 수 있습니다.
먼저 확인해야 할 것은 대상이 되는 페이지에 플러그인 아웃렛이 존재하는지입니다. 이를 돕는 테마 컴포넌트가 있습니다.
(deprecated) Plugin outlet locations theme component
해당 컴포넌트를 설치한 후, 토글을 켜고 대상이 되는 페이지로 이동하여 사용할 수 있는 아웃렛이 무엇인지 확인하세요. 이 경우, 해당 플러그인 아웃렛이 존재합니다(초록색으로 강조 표시됨).
따라서 우리가 원하는 아웃렛은 above-user-profile입니다.
만약 존재하지 않는다면 어떻게 해야 할까요? 이 경우 최선의 방법은 추가를 요청하거나 코어에 추가하기 위한 PR을 보내는 것입니다. 사용 사례가 타당하다면 대부분 승인됩니다.
어쨌든, 앞서 말했듯이 이 경우 이미 존재합니다. 이제 마크업을 추가하는 방법을 살펴봅시다. 나머지 부분은 위에서 언급한 컴포넌트가 필요하지 않으므로, 플러그인 아웃렛 이름을 이미 알고 있으므로 이제 비활성화해도 됩니다.
테마의 헤더 탭에 다음과 같은 코드를 추가하기만 하면 됩니다.
<script type="text/x-handlebars" data-template-name="/connectors/OUTLET_NAME/SOME_NAME">
Your markup goes here...
</script>
OUTLET_NAME을 대상이 되는 아웃렛의 이름으로 변경해야 합니다. 그런 다음 SOME_NAME을 이 커스터마이징에 부여할 이름으로 변경하세요. 이름은 아무거나 상관없지만, 설명적으로 짓는 것이 좋습니다. 좋은 관행입니다. 그래서 최종적으로 다음과 같이 됩니다.
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
Your markup goes here...like
<h1>Hello World!!</h1>
</script>
이것을 시도해보고 어떤 일이 일어나는지 살펴봅시다… 기억하세요, 위의 스니펫은 테마의 common > header 탭에 들어갑니다.
그리고…
그동안 잘 되고 있습니다. 하지만 더 깊이 파고들겠습니다.
비디오가 모든 프로필에 표시되기를 원하지 않고, 특정 조건에 따라만 표시되기를 원합니다. 그러면 어떻게 해야 할까요? 두 가지가 필요합니다. 소비할 데이터와 약간의 JavaScript입니다.
데이터를 찾아보겠습니다. 플러그인 아웃렛이 예약된 공간이라고 했을 때 기억나시나요? 컨텍스트 없이 그것들을 가질 목적은 무엇일까요? 그래서 Discourse는 각 플러그인 아웃렛에 관련 컨텍스트 조각을 전달합니다… 하지만 먼저 한 걸음 뒤로 물러나 보겠습니다. 여기에 이것을 추가할 때
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
Your markup goes here, like...
<h1>Hello World!!</h1>
</script>
HTML처럼 보이며 스크립트 태그도 있지만, 그 안의 내용은 handlebars 코드로 취급됩니다.
즉, 다음과 같은 것도 할 수 있다는 뜻입니다.
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
{{log this}}
</script>
그리고 브라우저 콘솔을 확인하세요. 아웃렛이 렌더링될 때마다, 즉 사용자 페이지에 있을 때 이것을 볼 수 있습니다.
이 중 도움이 되는 것이 있을까요? 네… 하지만 지금은 아닙니다. 나중에 다시 돌아오겠습니다. 한 걸음 더 뒤로 물러나서 Discourse가 컨텍스트를 아웃렛에 전달하는 방식을 살펴봅시다. Github에서 - 또는 로컬에서 - 아웃렛 이름을 검색하면 이것을 얻게 됩니다.
Repository search results · GitHub
해당 파일을 열어보겠습니다. 처음 보이는 것은 이 줄입니다.
그 줄의 마지막 부분을 살펴보세요.
args=(hash model=model)
Discourse가 model을 아웃렛의 인자로 전달하는 것을 볼 수 있습니다. 모든 의도와 목적을 위해, 그리고 단순화를 위해, model = data입니다.
즉, 우리 아웃렛의 인자 중 하나는 model이며, 우리가 원하는 데이터가 들어갈 곳입니다. 그래서 다시 우리 스니펫으로 돌아가보겠습니다.
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
{{log this}}
</script>
그리고 이것으로 변경해 보겠습니다.
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
- {{log this}}
+ {{log args}}
</script>
이제 콘솔에서 이것을 얻게 됩니다.
그 데이터를 탐색하여 필요한 것이 있는지 확인해 보세요. 해당 페이지의 다른 요소에서 사용되는 사용자에 대한 모든 데이터를 포함하고 있으므로 포함되어야 합니다. 그것은 해당 특정 사용자의 사용자 페이지의 "모델"입니다.
거기에서 사용할 수 있는 속성 중 하나는… 드럼롤
… 사용자가 속한 그룹입니다.
즉, 이렇게 하면
{{log args.model.groups}}
콘솔에 사용자가 속한 모든 그룹을 얻게 됩니다.
좋습니다, 이제 필요한 데이터를 가지고 있으므로 남은 일은 그것에 기반한 조건을 추가하는 것입니다.
같은 스니펫에서 그것을 할 수 있다고 생각할 수 있지만, 불행히도 그럴 수 없습니다. Handlebars는 템플릿 언어입니다. 논리에 대한 지원이 매우, 매우 기본적입니다 - 단순한 참/거짓 조건과 루프를 넘어가는 것은 없습니다. 비교와 같은 다른 작업을 수행할 수 없습니다.
그렇다면 정확히 어디서 그것을 수행할 수 있을까요? 커넥터 클래스(connector class)에서, 꽤 멋져 보이죠… 압니다.
간단히 말해, 커넥터 클래스는 본질적으로 아웃렛에 첨부된 JavaScript 조각입니다. 그것보다 훨씬 더 뉘앙스가 있지만, 지금은 이것만 알면 충분합니다.
자, 하나를 만들어 보겠습니다. 이렇게 합니다.
<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('OUTLET_NAME', 'SOME_NAME', {
});
</script>
여기서 OUTLET_NAME과 SOME_NAME은 위에서 사용한 것과 동일해야 합니다. 그래서 변경해 보겠습니다.
<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
});
</script>
이 스니펫도 테마의 common > header 탭에 들어갑니다. 따라서 이제 다음과 같은 것을 가지고 있어야 합니다.
<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
{{log args.model.groups}}
</script>
<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
});
</script>
커넥터 클래스 내부에서 일부 작업을 수행할 수 있습니다… 하지만… 그것은 일반적인 javaScript 파일과는 다르다는 점을 명심해야 합니다. 더 나은 설명이 부족하지만… 다이어트 중인 Ember 컴포넌트라고 생각하세요. 이 주제에 대해 더 깊이 들어가는 것은 여기서는 약간 벗어난 내용이므로 넘어가겠습니다.
기본적으로 네 가지 메서드가 연결되어 있습니다.
actions는 다음과 같이 동작을 정의할 수 있게 해줍니다.
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
actions: {
myAction() {
// do something
}
}
});
그런 다음 버튼이 눌릴 때와 같이 아웃렛 내부에서 해당 동작을 호출할 수 있습니다. 여기서는 이것이 필요하지 않으므로 넘어가겠습니다.
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
shouldRender(args, component) {
// return true or false here
}
});
아웃렛은 프로필 페이지에서만 렌더링되고 현재 다른 요구사항이 없으므로 이것도 사용하지 않을 것입니다. 그러나 아웃렛이 렌더링되기 전에 테스트하려는 모든 조건을 추가하는 데 사용할 수 있습니다. 예를 들어, 현재 사용자의 신뢰 수준(trust-level)이나 그런 것들입니다. 넘어가겠습니다…
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
setupComponent(args, component) {
// do something
}
});
이것이 우리가 집중해야 할 부분입니다. 설정하려는 모든 javaScript 조건이나 변수는 여기에 들어갑니다. 그 안에 더 깊이 들어가기 전에, 완성도를 위해 마지막 메서드를 먼저 다뤄보겠습니다.
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
teardownComponent(args, component) {
// do something
}
});
이것은 아웃렛이 제거될 때 발화됩니다. 따라서 이벤트 리스너 제거와 같이 필요한 모든 정리 작업을 수행할 수 있게 해줍니다.
좋습니다, setupComponent로 돌아가보겠습니다.
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
setupComponent(args, component) {
// do something
}
});
두 가지가 전달되는 것을 볼 수 있습니다. 먼저 args가 있고, 그 다음 component가 있습니다.
여기서 args는 앞서 살펴본 것과 동일한 것입니다. Discourse가 아웃렛에 전달한 컨텍스트 데이터입니다. 따라서 이렇게 하면
api.registerConnectorClass("above-user-profile", "add-profile-videos", {
setupComponent(args, component) {
console.log(args.model.groups);
}
});
이전에 본 것과 동일한 정보를 브라우저 콘솔에서 볼 수 있습니다. 프로필 소유자가 속한 그룹입니다. 여기서 재미있는 부분이 시작됩니다. 이제 데이터를 가지고 있고 올바른 훅(hook)을 가지고 있습니다. 따라서 여기서 원하는 것을何でも 할 수 있습니다. 예를 들어, 비디오가 특정 그룹에 속한 멤버의 프로필에서만 표시되기를 원한다면 이렇게 할 수 있습니다.
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
setupComponent(args, component) {
const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
const showVideo = inGroup.length ? true : false;
console.log(showVideo);
}
});
staff 그룹에 속한 사용자의 프로필 페이지에서 이것을 시도하면 콘솔에 true가 출력됩니다. 이제 우리가 해야 할 남은 일은 그것을 아웃렛 템플릿에 전달하는 것입니다. 이렇게 할 수 있습니다.
setupComponent에 전달되는 component는 커넥터와 아웃렛 사이에서 공유됩니다. 다음과 같이 컴포넌트에 속성으로 설정하여 아웃렛에 것을 전달할 수 있습니다.
const TARGET_GROUP = "staff"
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
setupComponent(args, component) {
const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
const showVideo = inGroup.length ? true : false;
- console.log(showVideo);
+ component.setProperties({showVideo})
}
});
이제 템플릿으로 돌아가서 다음과 같은 것을 하면
{{log showVideo}}
동일한 결과가 출력됩니다. 이제 그것을 Handlebars 조건에 넣고 마크업을 그 안에 다음과 같이 추가합니다.
<script
type="text/x-handlebars"
data-template-name="/connectors/above-user-profile/add-profile-videos"
>
{{#if showVideo}}
<video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
<source src="[INSERT LINK]" type="video/webm">
<source src="[INSERT LINK]" type="video/mp4">
</video>
{{/if}}
</script>
그런 다음 스태프 사용자의 프로필 페이지를 확인하세요. 비디오가 로드되는 것을 볼 수 있습니다.
스태프 멤버의 프로필에서 다른 곳으로 이동하면 비디오가 사라집니다. staff 그룹에 속하지 않은 사용자의 프로필에는 비디오가 표시되지 않습니다.
자, 이것을 모두 합쳐보겠습니다. 이것은 위에서 본 것과 동일한 내용입니다.
제가 사용한 CSS입니다. common > css 탭
#myVideo {
position: fixed;
top: var(--header-offset);
min-height: 100vh;
left: 0;
z-index: -1;
}
.user-content {
background: none;
}
.user-main {
padding: 0.5em;
background: rgba(var(--secondary-rgb), 0.8);
}
// 모바일에서도 원한다면
.mobile-view {
body[class*="user-"] {
background: none;
.user-main,
.user-content {
padding: 0.5em;
background: rgba(var(--secondary-rgb), 0.8);
}
}
}
HTML / javaScript / Handlebars. 이것은 테마의 common > header 탭에 들어갑니다.
<script
type="text/x-handlebars"
data-template-name="/connectors/above-user-profile/add-profile-videos"
>
{{#if showVideo}}
<video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
<source src="[INSERT LINK]" type="video/webm">
<source src="[INSERT LINK]" type="video/mp4">
</video>
{{/if}}
</script>
<script type="text/discourse-plugin" version="0.8">
const TARGET_GROUP = "staff"
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
setupComponent(args, component) {
const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
const showVideo = inGroup.length ? true : false;
component.setProperties({showVideo})
}
});
</script>
TARGET_GROUP을 대상이 되는 그룹의 이름으로 변경하고 비디오의 src 속성을 추가하세요.
이 게시글은 좀 길었습니다… 그것을 두려워하지 마세요. 개념을 파악하면, 위에서 한 모든 것은 3~5분 미만으로 수행할 수 있습니다.
여기서 좋은 점은 우리가 논의한 모든 것이 어떤 플러그인 아웃렛에 대해서도 거의 동일하다는 것입니다. 변하는 것은 이름뿐입니다. 따라서 이것은 미래에 수행하려는 모든 플러그인 아웃렛 수정에 적용됩니다.
- 아웃렛 이름 찾기
- 데이터 가져오기
- 커넥터에서 데이터 처리
- 템플릿으로 속성 전달




