# Cloudflare R2 이미지 URL 표시 문제: 상세 설명 및 해결 방법

**URL:** https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204
**Category:** Bug
**Tags:** chat
**Created:** [3월 20, 2025, 6:41오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204 "2025-03-20T18:41:59Z")
**Posts on this page:** 11
**Page:** 1

<div class="post-metadata">

### Author: ![qingfeng1024](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/qingfeng1024/32/495412_2.png) [@qingfeng1024](https://meta.discourse.org/u/qingfeng1024)
#### Post date: [3월 20, 2025, 6:41오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/1 "2025-03-20T18:41:59Z")

</div>

## 문제 설명

최근 포럼에서 이미지 표시에 문제가 발생하고 있으며, 구체적으로 다음과 같습니다:

- 이미지 섬네일(미리보기)이 올바르게 표시되지 않습니다.
- 이미지를 클릭하면 전체 크기 이미지가 올바르게 표시됩니다.
- 브라우저의 개발자 도구에서 잘못된 이미지 URL이 표시됩니다.

 ![이미지는 중국어로 된 채팅 인터페이스를 보여주며, 사용자가 URL 스크린샷과 관련된 일반적인 질문이나 추측에 대해 묻고 있음을 대화 버블과 빨간 화살표로 나타냅니다. (AI가 생성한 제목)](https://global.discourse-cdn.com/meta/original/4X/6/4/0/6401c760868402ca8e5434ea02dcf35b50ebf72d.png)

조사 결과, 문제의 근본 원인은 이미지 URL의 잘못된 도메인 이름임이 밝혀졌습니다:

- **올바른 URL:** `https://store.starorigin.cc/optimized/1X/[imageID].jpeg`
- **잘못된 URL:** `https://info.7a4081a2d83d3f43fe6b1be1c926fd1c.r2.cloudflarestorage.com/optimized/1X/[imageID].jpeg`

**시스템이 설정된 CDN 도메인 대신 원본 R2 버킷의 잘못된 도메인을 사용하고 있습니다.**

## 기술 분석

이것은 Cloudflare R2에 저장된 이미지를 처리할 때 Discourse에서 발생하는 알려진 문제입니다. `s3_cdn_url`이 설정되어 있는 경우에도, Discourse는 최적화된 이미지(예: 섬네일)를 생성할 때 CDN URL 대신 원본 저장소 URL을 사용할 수 있습니다.

이것은 다음과 같은 요소와 관련이 있을 수 있습니다:

- Discourse 버전
- S3 호환 저장소의 구성
- `OptimizedImage` 테이블에 URL이 저장되는 방식

## 해결책

가장 간단하고 효과적인 해결책은 클라이언트 측 수리를 위해 **Discourse 테마 컴포넌트** 를 사용하는 것입니다. 이것은 데이터베이스 작업이나 서버 구성 변경을 필요로 하지 않습니다. 브라우저에서 잘못된 URL을 올바른 URL로 자동으로 대체하는 짧은 JavaScript 코드 스니펫을 추가하는 것만 포함됩니다.

### 테마 컴포넌트 코드

```plaintext
import { apiInitializer } from "discourse/lib/api";

export default apiInitializer("0.11.1", (api) => {
  // Fix already loaded images
  function fixImageUrls() {
    const badDomain = "info.7a4081a2d83d3f43fe6b1be1c926fd1c.r2.cloudflarestorage.com";
    const goodDomain = "store.starorigin.cc";

    // Fix regular images
    document.querySelectorAll(`img[src*="${badDomain}"]`).forEach(img => {
      img.src = img.src.replace(badDomain, goodDomain);
    });

    // Fix lazy-loaded images
    document.querySelectorAll(`img[data-src*="${badDomain}"]`).forEach(img => {
      img.setAttribute('data-src', img.getAttribute('data-src').replace(badDomain, goodDomain));
    });

    // Fix background images
    document.querySelectorAll('[style*="background"]').forEach(el => {
      if (el.style.backgroundImage && el.style.backgroundImage.includes(badDomain)) {
        el.style.backgroundImage = el.style.backgroundImage.replace(badDomain, goodDomain);
      }
    });

    // Fix various other potential attributes
    ['srcset', 'data-large-src', 'data-small-src', 'data-download-href'].forEach(attr => {
      document.querySelectorAll(`[${attr}*="${badDomain}"]`).forEach(el => {
        el.setAttribute(attr, el.getAttribute(attr).replace(badDomain, goodDomain));
      });
    });
  }

  // Fix images in the editor
  api.decorateCooked($elem => {
    fixImageUrls();
  }, { id: 'fix-r2-image-urls' });

  // Fix after initial load
  api.onPageChange(() => {
    fixImageUrls();
  });

  // Handle dynamically loaded content
  const observer = new MutationObserver(mutations => {
    fixImageUrls();
  });

  // Start observing after the DOM is loaded
  if (document.readyState === "loading") {
    document.addEventListener('DOMContentLoaded', () => {
      fixImageUrls();
      startObserver();
    });
  } else {
    fixImageUrls();
    startObserver();
  }

  function startObserver() {
    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['src', 'data-src', 'srcset', 'style']
    });
  }
});

```

### 코드 작동 원리

이 코드는 다음 작업을 수행합니다:

1. **포괄적 감지:** 잘못된 도메인을 포함하는 모든 이미지 URL을 찾습니다.
2. **다양한 요소 처리:** 다양한 이미지 요소와 속성(img 태그, 지연 로드 이미지, 배경 이미지 등)을 처리합니다.
3. **동적 모니터링:** `MutationObserver`를 사용하여 페이지 변경 사항을 모니터링함으로써 동적으로 로드된 콘텐츠도 수정되도록 보장합니다.
4. **Discourse 통합:** 다양한 특수 시나리오를 처리하기 위해 Discourse API와 통합됩니다.

### 설치 단계

1. Discourse 관리자 계정에 로그인합니다.
2. **관리자** \> **사용자 정의** \> **테마 컴포넌트** 로 이동합니다.
3. **새로 만들기** 버튼을 클릭합니다.
4. **새 컴포넌트 만들기** 옵션을 선택합니다.
5. 이름을 “Fix R2 Image URLs”(또는 원하는 이름)으로 지정합니다.
6. “Javascript” 탭에서 위의 코드를 붙여넣습니다.
7. **생성** 버튼을 클릭합니다.
8. **활성화** 버튼을 클릭하고 적용할 테마를 선택합니다(보통 “기본값”).

### 검증

설치 후:

1. 포럼 페이지를 새로고침합니다.
2. 이미지를 포함하는 게시물을 확인합니다.
3. 섬네일이 올바르게 표시되는지 확인합니다.
4. 브라우저의 개발자 도구를 사용하여 이미지 요청이 모두 CDN 도메인을 가리키는지 확인합니다.

클라이언트 측 해결책은 특히 직접적인 서버 액세스가 제한된 경우 가장 간단하고 빠르며 위험이 낮은 접근 방식입니다.

## 결론

이 간단한 테마 컴포넌트는 서버 변경이나 복잡한 구성 없이 Discourse와 Cloudflare R2 스토리지 통합 시 이미지 URL 문제를 효과적으로 해결합니다. 근본 원인을 해결하는 것이 아니라 클라이언트 측에서 문제를 수정하지만, 구현이 쉽고 즉각적인 결과를 제공하며 이상적인 해결책입니다.

귀하의 Discourse 사이트에도 유사한 문제가 발생하고 있다면, 이 해결책을 시도해 보시기 바랍니다.

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [3월 23, 2025, 10:43오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/2 "2025-03-23T22:43:55Z")

</div>

이 문제가 채팅에만 영향을 미치는 건가요? 범위를 파악해 보고 있습니다.

매우 유용한 보고를 올려주셔서 감사합니다!

---

<div class="post-metadata">

### Author: ![qingfeng1024](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/qingfeng1024/32/495412_2.png) [@qingfeng1024](https://meta.discourse.org/u/qingfeng1024)
#### Post date: [3월 24, 2025, 12:01오전 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/4 "2025-03-24T00:01:08Z")

</div>

네, 이 버그는 채팅에만 영향을 미칩니다. 제 S3 서비스 제공자는 Cloudflare R2입니다. 채팅 인터페이스에서 이미지를 전송할 때 기본 CDN 링크 설정을 사용할 수 없어 이미지가 로드되지 않습니다.

---

<div class="post-metadata">

### Author: ![DevTeVe](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/devteve/32/487601_2.png) [@DevTeVe](https://meta.discourse.org/u/DevTeVe)
#### Post date: [10월 31, 2025, 5:33오전 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/7 "2025-10-31T05:33:12Z")

</div>

여전히 [Upload images in chat can't be show normally when use s3 CDN](https://meta.discourse.org/t/upload-images-in-chat-cant-be-show-normally-when-use-s3-cdn/365540?u=devteve) 사례와 같은 문제인 것 같으며, 수정이 시도되었으나 나중에 되돌려졌습니다.

---

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [5월 29, 2026, 7:57오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/8 "2026-05-29T19:57:27Z")

</div>

도메인 설정이 포함된 업데이트된 테마 컴포넌트로 이 문제를 해결했습니다. 이제 누구나 자체 컴포넌트를 하드코딩하지 않고도 이를 사용할 수 있습니다. 이는 모든 S3 호환 저장소에서 작동합니다:

> **[GitHub - Lillinator/chat-s3-thumbnails-fix: Fixes the chat thumbnail path bug for Cloudflare...](https://github.com/Lillinator/chat-s3-thumbnails-fix)**
>
> Fixes the chat thumbnail path bug for Cloudflare R2 object storage uploads

> **직접 패치하는 경우, 이니셜라이저는 다음과 같이 작성해야 합니다**
>
> ```js
> import { apiInitializer } from "discourse/lib/api";
> 
> export default apiInitializer("1.8.0", (api) => {
> 
> // ⚠️ 이 도메인이 정확히 여러분의 도메인인지 확인하세요!
> // 게시글 썸네일 및 채팅 썸네일의 이미지 경로를 확인하고 비교해 보세요!
> 
> const badDomain = "yoursite-uploads.xxx.r2.cloudflarestorage.com";
> const goodDomain = "uploads.yoursite.com";
> 
> function fixImage(img) {
> if (img.src && img.src.includes(badDomain)) {
> img.src = img.src.replace(badDomain, goodDomain);
> }
> if (img.dataset?.src && img.dataset.src.includes(badDomain)) {
> img.dataset.src = img.dataset.src.replace(badDomain, goodDomain);
> }
> }
> 
> // 1. 표준 게시글/HTML에서 렌더링된 이미지를 안전하게 수정
> api.decorateCooked($elem => {
> const container = $elem[0];
> if (!container) return;
>     
> container.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
> }, { id: 'fix-r2-chat-bug' });
> 
> // 2. 동적으로 주입된 채팅 요소에만 적용되는 고성능 옵저버
> const observer = new MutationObserver(mutations => {
> for (const mutation of mutations) {
>       
> // 새 채팅 메시지나 요소가 DOM에 주입된 경우
> if (mutation.type === 'childList') {
> for (const node of mutation.addedNodes) {
> if (node.nodeType === 1) { // ELEMENT_NODE
> if (node.tagName === 'IMG') fixImage(node);
>             
> // 새 노드 내부에서만 검색하며, 문서 전체를 검색하지 않음
> node.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
> }
> }
> } 
> // 기존 이미지의 src 속성이 변경된 경우 (레이지 로딩)
> else if (mutation.type === 'attributes') {
> if (mutation.target.tagName === 'IMG') {
> fixImage(mutation.target);
> }
> }
> }
> });
> 
> // 옵저버를 즉시 시작
> observer.observe(document.body, {
> childList: true,
> subtree: true,
> attributes: true,
> attributeFilter: ['src', 'data-src']
> });
> });
> 
> ```

---

<div class="post-metadata">

### Author: ![David\_Ghost](https://avatars.discourse-cdn.com/v4/letter/d/c37758/32.png) [@David\_Ghost](https://meta.discourse.org/u/David_Ghost)
#### Post date: [6월 6, 2026, 2:58오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/9 "2026-06-06T14:58:48Z")

</div>

이 문제는 채팅에서만 발생하는 것이 아닌 것 같습니다. 방금 포럼을 R2로 이전했는데, 새로 업로드된 이미지들은 모두 S3 CDN이라는 올바른 경로를 가지고 있지만, 기존 이미지들은 엔드포인트 주소를 사용하고 있습니다. 그리고 제 경우엔 @Lilly님의 TC를 설치해야만 표시가 됩니다.

샘플을 몇 가지 첨부합니다:

 ![sp](https://global.discourse-cdn.com/meta/original/4X/7/a/d/7adef004c4e71870dba00f9c889ccbb65b78b9d9.png)  
 ![sp2](https://global.discourse-cdn.com/meta/original/4X/c/9/0/c900ca48e6d9aa4e01bf75a1a98833d1c1bb32e9.png)

---

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [6월 6, 2026, 3:17오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/10 "2026-06-06T15:17:30Z")

</div>

오래된 이미지의 경우(아주 새로운 포럼이 아닌 경우) 다음 rake 작업을 실행해야 합니다:

```plaintext
cd /var/discourse
./launcher enter app
rake uploads:migrate_to_s3

```

그 다음 로컬 업로드를 다시 베이킹합니다:

```plaintext
rake posts:rebake_match["/uploads/default/original/"]

```

기억해 줘서 감사합니다. 이 내용을 제 R2 객체 저장소 문서에 추가하겠습니다.

---

<div class="post-metadata">

### Author: ![David\_Ghost](https://avatars.discourse-cdn.com/v4/letter/d/c37758/32.png) [@David\_Ghost](https://meta.discourse.org/u/David_Ghost)
#### Post date: [6월 6, 2026, 3:24오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/11 "2026-06-06T15:24:42Z")

</div>

> [@Lilly](#):
>
> `rake uploads:migrate_to_s3`

네, 이미 그 방법을 시도해 보았지만, 여전히 엔드포인트 URL이 다음과 같은 형태로 표시됩니다:  
([https://bucketname.xxxx.r2.cloudflarestorage.com/original/1X/ac9e68ba07090c0d2d8fd3e38338e75e1e328448.jpeg](https://bucketname.xxxx.r2.cloudflarestorage.com/original/1X/ac9e68ba07090c0d2d8fd3e38338e75e1e328448.jpeg))

---

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [6월 6, 2026, 3:35오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/12 "2026-06-06T15:35:39Z")

</div>

이 줄을 `app.yml`에 추가했거나(`S3 CDN URL` 관리자 설정에 도메인을 추가했거나) 하셨나요?

```plaintext
DISCOURSE_S3_CDN_URL: https://your.R2.domain.com # (실제 R2 커스텀 도메인)

```

모든 작업이 올바른 순서로 수행되어야 합니다.

1. 관리자/`app.yml` 설정
2. rake migrate
3. 포스트 재배기 (rebake)

도움이 될 수 있는 문자열 치환 도구도 있습니다 - 컨테이너 내에서 실행하세요 (문자열을 특정 값으로 교체):

```plaintext
discourse remap "https://<cloudflare-account-id>.r2.cloudflarestorage.com/<R2-bucket-name>" "https://your.cdn.domain.com"

```

---

<div class="post-metadata">

### Author: ![David\_Ghost](https://avatars.discourse-cdn.com/v4/letter/d/c37758/32.png) [@David\_Ghost](https://meta.discourse.org/u/David_Ghost)
#### Post date: [6월 7, 2026, 2:57오전 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/13 "2026-06-07T02:57:18Z")

</div>

네, 모든 것이 정상적으로 보이고 리맵으로도 해결되지 않습니다. 이전 썸네일 여전히 깨져 있지만, 이미지를 클릭하거나 TC를 활성화하면 이미지가 표시됩니다.

---

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [6월 7, 2026, 1:01오후 UTC](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204/14 "2026-06-07T13:01:07Z")

</div>

안 돼서 죄송합니다.

버그를 수정하는 PR을 여기에 올렸습니다:

> [@채팅 섬네일이 s3\_cdn\_url을 우회하고 S3 버킷 원본 URL을 사용함](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174):
>
> 최근 Cloudflare R2 업로드 버킷 구성을 했는데 채팅 썸네일 이미지가 깨졌습니다. 그래서 조사를 해보니 제 구성에 대한 빠른 해결책을 찾았고, 이 게시글을 발견했습니다: [Cloudflare R2 Image URL Display Issue: Detailed Explanation and Fix](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204). 어쨌든 다른 S3 업로드 버킷 구성을 살펴보니 이 버그가 Cloudflare에 특화된 문제가 아니라는 것을 알게 되었습니다. 설명 외부 S3 또는 호환되는 오브젝트 스토리지가 업로드로 구성되었을 때, 채팅 썸네일 이미지는 CDN을 우회하고 버킷 URL에서 직접 로드됩니다. 보안 강화된 외부 S3 호환 버킷(예: Cloudflare R2)의 경우, 채팅 썸네일이 깨져서 표시되지 않습니다. 근본적인 문제는 채팅 직렬화기(serializer)가 썸네일에 s3\_cdn\_url 설정을 적용하는 데 실패한다는 것입니다. 구성된 CDN을 통해 이미지를 라우팅하는 대신, 브라우저 페이로드에 원본…
