임베드가 “토론 로딩 중…”에 멈추거나 주제가 생성되지 않나요? 해결 방법 안내

오늘 업무를 재개하면서 관리 페이지에서 몇 가지 눈에 띄는 부분이 있었습니다. 개선할 수 있는 부분에 대한 몇 가지 생각을 정리해 보았습니다.

임베딩 설정 — /admin/customize/embedding/settings

allowed_internal_hosts는 비공개 환경에서 임베딩이 안정적으로 작동하기 위한 핵심 설정입니다. 이 섹션에서 관련 설정으로 명시적으로 나열되어야 하며, 그 중요성에 대해서는 의문의 여지가 없습니다.

임베딩 호스트 — /admin/customize/embedding

제공된 구성 스니펫은 많은 유용한 정보를 포함하고 있어 매우 도움이 됩니다. 이를 활용하여 추가적인 가이드를 제공할 수 있다고 생각합니다.

첫 번째 문단

현재:

다음 HTML 코드를 사이트에 붙여넣어 Discourse 토픽을 생성하고 임베드하세요. EMBED_URL을 임베드하려는 페이지의 정준 URL(canonical URL)로 교체하십시오.

대안:

댓글이 표시될 페이지의 원하는 위치에 다음 HTML을 붙여넣으세요.
discourseEmbedUrl은 해당 페이지의 URL이며, Discourse에서 링크로 연결될 대상입니다. 페이지가 처음 로드될 때 Discourse는 해당 URL에 대한 토픽을 찾거나 생성하고 콘텐츠로 다시 링크합니다.

두 번째 문단

현재:

스타일을 사용자 정의하려면 주석 해제 후 CLASS_NAME을 테마의 임베디드 CSS에 정의된 CSS 클래스로 교체하십시오.

대안:

className 속성을 사용하여 임베드된 iframe 내부의 <html> 태그에 사용자 정의 클래스를 추가합니다. 스타일을 적용하려면 /admin/customize/themes로 이동하여 테마의 편집 버튼을 클릭한 뒤 코드 편집 버튼을 누르고 고급 보기를 선택하세요. 사용자 정의 CSS를 임베디드 CSS 섹션에 추가합니다.

세 번째 문단

현재:

DISCOURSE_USERNAME을 토픽을 생성해야 할 작성자의 Discourse 사용자 이름으로 교체하십시오. Discourse는 name 속성이 discourse-username 또는 author로 설정된 <meta> 태그의 content 속성을 통해 사용자를 자동으로 조회합니다. discourseUserName 매개변수는 비활성화(deprecated)되었으며 Discourse 3.2에서 제거될 예정입니다.

대안:

참고: 토픽은 실제 Discourse 사용자가 생성합니다. 표시 이름이나 작성자 문자열이 아닙니다. 유효하고 존재하는 계정이여야 합니다. 어떤 사용자가 사용되는지를 결정하는 방법은 세 가지가 있습니다:

  1. 기본 폴백 — /admin/customize/embedding/posts_and_topics에서 설정
  2. 호스트별 오버라이드 — /admin/customize/embedding/ 하위에서 설정
  3. URL별 제어 — 페이지에 존재하는 Discourse USERNAME을 가진 <meta name="discourse-username" content="USERNAME"> 태그를 추가

기존 Discourse 사용자의 사용자 이름만 작동합니다. 메타 태그의 사용자가找不到되지 않으면 Discourse는 호스트 수준 또는 글로벌 기본값으로 폴백합니다. 여기에 소개된 <meta> 태그 방식은 토픽 생성에 사용되는 Discourse 사용자를 프로그램 방식으로 URL별로 제어할 수 있게 해줍니다. 예를 들어, 사이트의 블로그 글 작성자를 일치하는 Discourse 사용자 계정으로 매핑할 수 있습니다.

구성 스니펫 섹션

접이식 “구성 스니펫” 섹션은 놓치기 쉽습니다. 시각적으로 제목과 유사하며, 미묘한 화살표는 직관적이지 않습니다. 눈에 띄는 색상으로 처리된 “자세히 알아보기” 링크와 달리, 이 섹션은 마치 숨겨져 있는 것처럼 보입니다.

이것이 논쟁의 여지가 있다는 것을 알겠습니다. 일부는 UI가 깔끔하고 충분하다고 느낄 수 있습니다. 하지만 저는 이것이 클릭 가능한 것을 깨달을 때까지 이 섹션을 너무 자주 놓쳤습니다. 이는 특히 예시에 의존하는 초보자를 위해 약간의 시각적 단서 개선이나 비접기(non-collapsed) 기본값 설정이 큰 도움이 될 수 있음을 보여줍니다.

스니펫

현재:

<div id='discourse-comments'></div>
  <meta name='discourse-username' content='DISCOURSE_USERNAME'>

  <script type="text/javascript">
    DiscourseEmbed = {
      discourseUrl: 'https://discourse.your-site.com/',
      discourseEmbedUrl: 'EMBED_URL',
      // className: 'CLASS_NAME',
    };

    (function() {
      var d = document.createElement('script'); d.type = 'text/javascript'; d.async = true;
      d.src = DiscourseEmbed.discourseUrl + 'javascripts/embed.js';
      (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(d);
    })();
  </script>

대안:

<div id="discourse-comments"></div>
<!-- 선택 사항: 토픽을 생성할 Discourse 사용자 계정을 지정 -->
<!-- 생략할 경우, Discourse는 호스트별 또는 글로벌 기본 사용자로 폴백합니다 -->
<meta name="discourse-username" content="DISCOURSE_USERNAME" />

<script type="text/javascript">
  DiscourseEmbed = {
    discourseUrl: 'https://discourse.mydomain.com/', // 끝부분 슬래시 필수
    discourseEmbedUrl: window.location.href, // 또는 하드코딩된 정준 URL 문자열
    // className: 'my-iframe-theme another-class',
    // discourseReferrerPolicy: 'strict-origin-when-cross-origin',
    // topicId: '1234',
  };

  (function() {
    const d = document.createElement('script');
    d.type = 'text/javascript';
    d.async = true;
    d.src = `${DiscourseEmbed.discourseUrl}javascripts/embed.js`;
    document.head.appendChild(d);
  })();
</script>

DiscourseEmbed 옵션 — 간략한 설명

  • discourseUrl — 필수
    Discourse 인스턴스의 전체 URL입니다. 끝부분에 슬래시가 있어야 하며, 예: https://discourse.mydomain.com/

  • discourseEmbedUrl — 필수
    댓글이 임베드되는 현재 페이지의 전체 URL입니다. Discourse가 이를 통해 토픽을 식별하고 콘텐츠에 링크합니다.

  • className — 선택 사항
    iframe 내부의 <html> 요소에 사용자 정의 CSS 클래스를 추가합니다. Discourse 테마의 “임베디드 CSS” 섹션에서 스타일을 정의하세요.

  • discourseReferrerPolicy — 선택 사항
    기본값은 no-referrer-when-downgrade입니다. 참고: Referrer-Policy

  • topicId — 선택 사항
    설정된 경우, Discourse는 해당 토픽을 직접 사용합니다. 그렇지 않으면 discourseEmbedUrl과 일치하는 토픽을 찾거나, 없으면 생성합니다.

DiscourseEmbed 컨텍스트 문서화

discourseEmbedUrl은 Discourse 서버에서 접근 가능해야 합니다. iframe이 로드될 때 Discourse는 해당 URL의 페이지를 가져와 토픽을 생성하거나 위치를 파악합니다. 이는 공개 플랫폼에 호스팅된 웹사이트에서 원활하게 작동합니다.

하지만 로컬에서 개발 중이라면, 임베딩이 "토론 로드 중…"에 멈추거나 아예 표시되지 않을 수 있습니다. 이는 로컬 개발 URL(예: localhost)이 Discourse의 SSRF 보호나 force_https 옵션, 또는 누락된 임베드 가능 호스트에 의해 차단될 수 있기 때문입니다.

기존 사이트의 새로운 기능을 개발 중이라면, discourseEmbedUrl을 프로덕션 URL로 지정하는 것이 하나의 해결책입니다. embed_any_origin이 활성화되어 있으면, iframe이 다른 오리진에서 서빙되더라도 Discourse는 임베딩이 작동하도록 허용합니다. 댓글이 존재하면 로드되고, 없으면 “토론 계속” 버튼이 표시됩니다.

대안적으로, 로컬 도메인(예: localhost)을 임베딩 호스트에 추가했다면 embed_any_origin이 전혀 필요 없을 수 있습니다. 하지만 여전히 localhost를 임베드 가능 호스트로 추가해야 합니다.

:warning: 주의할 점: force_https 설정이 활성화되어 있고 개발 사이트가 TLS를 사용하지 않으면 임베딩이 실패합니다. 이 경우 개발 중 force_https를 비활성화하거나 테스트를 위해 별도의 Discourse 인스턴스를 구동하는 것을 고려해 보세요.

참고: discourseEmbedUrl이 공개적으로 접근 가능하고 임베딩이 여전히 "토론 로드 중…"을 표시하면서 토픽을 생성하지 않는다면, 도메인이 Discourse의 SSRF 보호에 의해 차단되었을 수 있습니다.

이러한 문제는 로컬 DNS 해석이 이루어지는 환경(예: Docker, Kubernetes, 내부 DNS 서버가 있는 LAN)에서 Discourse 인스턴스가 실행될 때 자주 발생합니다. 이 경우 Discourse는 사이트의 도메인을 로컬 IP 주소(예: 127.0.0.1 또는 192.168.x.x)로 해석하고 이를 안전하지 않다고 판단할 수 있습니다.

접근을 허용하려면 도메인을 allowed_internal_hosts 사이트 설정에 추가하세요. 이는 도메인을 안전하게 가져올 수 있도록 명시적으로 표시하여 SSRF 필터링을 우회합니다.

차단된 IP 범위의 전체 목록은 Discourse 소스 코드에서 확인할 수 있습니다.

허용된 내부 호스트 — 사이트 설정 설명

현재:

oneboxing 등 기타 목적으로 Discourse가 안전하게 크롤링할 수 있는 내부 호스트 목록

대안:

내부 IP로 해석되는 호스트를 Discourse가 크롤링할 수 있도록 허용합니다. 사이트가 로컬 DNS(예: Docker, LAN, Kubernetes) 뒤에 실행될 때 필요합니다. SSRF 보호가 액세스를 차단하는 경우 댓글 임베딩, 토픽 생성, oneboxing에 필수적입니다.

고려 사항

이러한 제안 중 일부는 공식 문서에 대한 링크로 더 적합할 수 있습니다. 사실, 이 게시물만으로도 충분한 역할을 할 수 있으며, 다른 게시물처럼 인덱싱되어야 하기 때문입니다. 다른 제안들은 적절한 풀 리퀘스트를 필요로 할 수 있으며, 저는 언젠가 처리하겠지만 오늘은 아닐 것입니다.

그럼에도 불구하고, 제가 작성한 내용에는 기술적 부정확성이 있을 수 있습니다. 대부분은 실무 경험을 바탕으로 했지만, 동작을 오해했거나 캐싱에 속았거나(아, 캐싱…), 단순히 무언가를 놓쳤을 수 있습니다. 그 부분은 숙련된 Discourse 베테랑들의 판단에 맡기겠습니다.