# 최근 답변이 표시되는 데 2초가 걸립니다

**URL:** https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041
**Category:** Development
**Created:** [5월 6, 2025, 7:02오후 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041 "2025-05-06T19:02:34Z")
**Posts on this page:** 8
**Page:** 1

<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: [5월 6, 2025, 7:02오후 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/1 "2025-05-06T19:02:34Z")

</div>

가장 최근의 답변을 즉시 표시하는 방법이 있을까요? 현재 이 코드를 사용 중입니다:

```plaintext
  api.onPageChange(() => {
    if (window.location.pathname === "/") {
      const container = document.querySelector(".latest-topic-list");
      if (!container || container.dataset.modified === "true") return;

      fetch("/posts.json?order=created")
        .then(res => res.json())
        .then(data => {
          const replies = data.latest_posts
            .filter(p => p.post_number > 1 && !p.topic_slug.includes("private-message"))
            .slice(0, 15);

          const topicFetches = replies.map(post =>
            fetch(`/t/${post.topic_id}.json`)
              .then(res => res.json())
              .then(topic => {
                return {
                  post,
                  category: topic.category_id ? topic.category_name : null,
                  tags: topic.tags || []
                };
              })
          );

          Promise.all(topicFetches).then(results => {
            const rows = results.map(({ post, category, tags }) => {
              const url = `/t/${post.topic_slug}/${post.topic_id}/${post.post_number}`;
              const avatarUrl = post.avatar_template.replace("{size}", "45");
              const excerpt = post.excerpt?.replace(/<\/?[^>]+(>|$)/g, "")?.slice(0, 120) + (post.excerpt?.length > 120 ? '...' : '') || '';

              const categoryHtml = category
                ? `<span style="font-size: 0.85em; color: #666;">Categoria: <strong>${category}</strong></span><br>`
                : '';

              const tagsHtml = tags.length
                ? `<span style="font-size: 0.85em; color: #666;">Tags: ${tags.map(tag => `<span style="background:#eee; padding:2px 6px; border-radius:3px; margin-right:4px;">${tag}</span>`).join("")}</span>`
                : '';

              return `
                <tr class="topic-list-item">
                  <td class="main-link clearfix">
                    <div style="display: flex; align-items: center; gap: 16px; padding: 8px 0;">
                      <div style="flex-shrink: 0;">
                        <a class="avatar-link" href="/u/${post.username}">
                          <img loading="lazy" width="45" height="45" src="${avatarUrl}" class="avatar" alt="${post.username}">
                        </a>
                      </div>
                      <div style="display: flex; flex-direction: column; justify-content: center; padding-top: 8px; padding-bottom: 8px;">
                        <span class="link-top-line" style="margin-bottom: 6px;">
                          <a href="${url}" class="title raw-link">${excerpt}</a>
                        </span>
                        <div class="link-bottom-line">
                          ${categoryHtml}
                          ${tagsHtml}
                        </div>
                      </div>
                    </div>
                  </td>
                </tr>
              `;
            }).join("");

            // Cria o contêiner da seção de últimos comentários
            const latestRepliesContainer = document.createElement("div");
            latestRepliesContainer.className = "latest-replies-container";
            latestRepliesContainer.style.marginTop = "2em";

            latestRepliesContainer.innerHTML = `
              <table class="topic-list latest-topic-list">
                <thead>
                  <tr>
                    <th class="default">Últimos Comentários</th>
                  </tr>
                </thead>
                <tbody>
                  ${rows}
                </tbody>
              </table>
            `;

            container.parentNode.insertBefore(latestRepliesContainer, container.nextSibling);
            container.dataset.modified = "true";
          });
        })
        .catch(error => {
          console.error("Erro ao buscar últimos comentários:", error);
        });
    }
  });
</script>

```

하지만 콘텐츠가 표시되는 데 평균 2초 정도 걸립니다. 오른쪽 사이드바의 “최근 답변” 블록에서도 동일한 현상이 발생합니다. 이것이 정상적인가요?

---

<div class="post-metadata">

### Author: ![supermathie](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/supermathie/32/507518_2.png) [@supermathie](https://meta.discourse.org/u/supermathie)
#### Post date: [5월 6, 2025, 9:11오후 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/2 "2025-05-06T21:11:11Z")

</div>

> [@David\_Ghost](#):
>
> 현재 이 코드를 사용 중입니다.

왜 코드를 추가하시나요?

이것은 특별한 설정 없이 바로 작동합니다. 당신에게는 오류가 발생하나요?

---

<div class="post-metadata">

### Author: ![Arkshine](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/arkshine/32/298682_2.png) [@Arkshine](https://meta.discourse.org/u/Arkshine)
#### Post date: [5월 7, 2025, 12:42오전 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/3 "2025-05-07T00:42:08Z")

</div>

> [@David\_Ghost](#):
>
> 하지만 콘텐츠가 표시되는 데 평균 2초가 걸립니다.

그분의 문제입니다.

코드가 여러 API 요청을 수행하기 때문에 예상되는 현상이라고 봅니다.  
최신 게시물을 가져온 후, 카테고리 이름을 가져오기 위해 각 주제 ID(여기서는 15개)마다 요청을 한 번씩 보내고 있습니다.

현재 플러그인을 사용하여 맞춤형 SQL 쿼리를 실행하는 것 외에는 다른 방법이 있는지 모릅니다.

---

<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: [5월 7, 2025, 4:47오전 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/4 "2025-05-07T04:47:37Z")

</div>

정확합니다.  
하지만 [오른쪽 사이드바 블록](https://meta.discourse.org/t/right-sidebar-blocks/231067)도 요청을 보내나요? 최근 답변에 대해 동일한 결과를 반환합니다. 표시되는데 2초가 걸립니다.

플러그인을 만드는 방법을 알아볼게요. 감사합니다

---

<div class="post-metadata">

### Author: ![Arkshine](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/arkshine/32/298682_2.png) [@Arkshine](https://meta.discourse.org/u/Arkshine)
#### Post date: [5월 7, 2025, 11:32오전 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/5 "2025-05-07T11:32:44Z")

</div>

네, 최신 게시물을 가져오는 방식은 동일합니다. 하지만 거기까지입니다. 카테고리 이름을 가져오려고 하지 않는데, 이것이 차이점입니다.

---

<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: [5월 7, 2025, 11:51오전 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/6 "2025-05-07T11:51:36Z")

</div>

알겠습니다. 도움을 주셔서 감사합니다.

다른 해결책을 찾아보겠습니다.

---

<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: [5월 7, 2025, 4:42오후 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/7 "2025-05-07T16:42:06Z")

</div>

결국, 지금은 이것을 사용하고 있습니다:

```plaintext
<script type="text/discourse-plugin" version="0.11.3">
  api.onPageChange(() => {
    if (window.location.pathname === "/") {
      const container = document.querySelector(".latest-topic-list");
      if (!container) return;
      
      // 초기화 중복 방지
      if (window.latestRepliesInitialized) return;
      window.latestRepliesInitialized = true;
      
      // 설정
      const POLLING_INTERVAL = 2000; // 2초
      const COMMENTS_TO_SHOW = 15;
      const CACHE_DURATION = 30 * 60 * 1000; // 30분
      
      // 캐시 키
      const CACHE_KEY = "discourse_latest_replies_data";
      const CACHE_TIMESTAMP_KEY = "discourse_latest_replies_timestamp";
      const CACHE_LAST_ID_KEY = "discourse_latest_replies_last_id";
      
      // 비교를 위해 마지막으로 확인된 게시글 ID 저장
      let lastSeenPostId = parseInt(localStorage.getItem(CACHE_LAST_ID_KEY) || "0");
      let pollingIntervalId = null;
      
      console.log(`최근 댓글 플러그인 초기화 중 (캐시된 마지막 ID: ${lastSeenPostId})`);
      
      // 댓글을 불러오는 함수
      function loadLatestReplies(silent = false, forceRefresh = false) {
        // 강제 새로고침이 아닌 경우 먼저 캐시를 확인합니다.
        if (!forceRefresh) {
          const cachedData = localStorage.getItem(CACHE_KEY);
          const cacheTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
          const now = Date.now();
          
          // 유효한 캐시 데이터가 있는 경우
          if (cachedData && cacheTimestamp && now - parseInt(cacheTimestamp) < CACHE_DURATION) {
            try {
              const results = JSON.parse(cachedData);
              console.log(`캐시된 데이터 사용 중 (${results.length}개 댓글, ${Math.round((now - parseInt(cacheTimestamp)) / 1000 / 60)}분 전의 캐시)`);
              renderLatestReplies(results, false);
              
              // 조용한 모드가 아니라면 더 이상 할 일이 없습니다.
              if (!silent) {
                return;
              }
              
              // 조용한 모드라면 업데이트 확인을 위해 계속 진행합니다.
            } catch (e) {
              console.error("캐시 처리 중 오류:", e);
              // 캐시에 오류가 있으면 최신 데이터를 가져오기 위해 계속 진행합니다.
            }
          } else if (cachedData) {
            console.log("캐시 만료, 최신 데이터 가져오는 중");
          } else {
            console.log("캐시 없음, 최신 데이터 가져오는 중");
          }
        } else {
          console.log("새로고침 강제, 캐시 무시");
        }
        
        // 조용한 모드가 아니라면 로딩 표시기를 표시합니다.
        if (!silent) {
          // 이미 댓글 컨테이너가 존재하는 경우 표시기를 표시하지 않습니다.
          const existingContainer = document.querySelector(".latest-replies-container");
          if (!existingContainer) {
            let loadingIndicator = document.getElementById("latest-replies-loading");
            if (!loadingIndicator) {
              loadingIndicator = document.createElement("div");
              loadingIndicator.id = "latest-replies-loading";
              loadingIndicator.innerHTML = `
                <div style="text-align: center; padding: 20px;">
                  <span class="spinner small"></span>
                  <span style="margin-left: 10px;">최근 댓글 로딩 중...</span>
                </div>
              `;
              container.parentNode.insertBefore(loadingIndicator, container.nextSibling);
            }
          }
        }
        
        // 최신 데이터를 가져옵니다.
        fetch("/posts.json?order=created")
          .then(res => res.json())
          .then(data => {
            // 진단용 로그
            if (!silent) {
              console.log("API에서 받은 데이터:", data.latest_posts.length);
            }
            
            const replies = data.latest_posts
              .filter(p => p.post_number > 1 && !p.topic_slug.includes("private-message"))
              .slice(0, COMMENTS_TO_SHOW);
            
            // 새 게시글이 있는지 확인합니다.
            const maxId = replies.length > 0 ? Math.max(...replies.map(post => post.id)) : 0;
            const hasNewPosts = maxId > lastSeenPostId;
            
            // 진단용 로그
            if (hasNewPosts && !silent) {
              console.log(`새 게시글 감지됨. 마지막 ID: ${lastSeenPostId}, 새로운 최대 ID: ${maxId}`);
            }
            
            // 마지막으로 확인된 ID를 업데이트합니다.
            if (maxId > lastSeenPostId) {
              lastSeenPostId = maxId;
              localStorage.setItem(CACHE_LAST_ID_KEY, lastSeenPostId.toString());
            }
            
            // 새 게시글이 없고 조용한 확인인 경우 아무것도 하지 않습니다.
            if (!hasNewPosts && silent && !forceRefresh) {
              return;
            }
            
            // 토픽 세부 정보를 가져옵니다.
            const topicPromises = replies.map(post => {
              // 토픽이 캐시에 있는지 확인합니다.
              const topicCacheKey = `discourse_topic_${post.topic_id}`;
              const cachedTopic = localStorage.getItem(topicCacheKey);
              
              if (cachedTopic && !forceRefresh) {
                try {
                  return Promise.resolve(JSON.parse(cachedTopic));
                } catch (e) {
                  console.error(`토픽 ${post.topic_id} 캐시 처리 중 오류:`, e);
                  // 캐시에 오류가 있으면 서버에서 가져옵니다.
                }
              }
              
              return fetch(`/t/${post.topic_id}.json`)
                .then(res => res.json())
                .then(topic => {
                  // 토픽을 캐시에 저장합니다.
                  localStorage.setItem(topicCacheKey, JSON.stringify(topic));
                  return topic;
                })
                .catch(error => {
                  console.error(`토픽 ${post.topic_id} 가져오기 오류:`, error);
                  return { category_id: null, category_name: null, tags: [] };
                });
            });
            
            Promise.all(topicPromises)
              .then(topics => {
                const results = replies.map((post, index) => {
                  const topic = topics[index];
                  return {
                    post,
                    category: topic.category_id ? topic.category_name : null,
                    tags: topic.tags || []
                  };
                });
                
                // 결과를 캐시에 저장합니다.
                localStorage.setItem(CACHE_KEY, JSON.stringify(results));
                localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
                
                // 결과를 렌더링합니다.
                renderLatestReplies(results, hasNewPosts || forceRefresh);
              })
              .catch(error => {
                console.error("토픽 처리 중 오류:", error);
              });
          })
          .catch(error => {
            console.error("최근 댓글 가져오기 오류:", error);
            // 오류 발생 시 로딩 표시기를 제거합니다.
            if (!silent) {
              const loadingElement = document.getElementById("latest-replies-loading");
              if (loadingElement) loadingElement.remove();
            }
          });
      }
      
      // 결과를 렌더링하는 함수
      function renderLatestReplies(results, animate = false) {
        // 로딩 표시기를 제거합니다.
        const loadingElement = document.getElementById("latest-replies-loading");
        if (loadingElement) loadingElement.remove();
        
        // 결과가 없으면 아무것도 하지 않습니다.
        if (!results || results.length === 0) {
          console.log("표시할 댓글을 찾을 수 없음");
          return;
        }
        
        const rows = results.map(({ post, category, tags }) => {
          const url = `/t/${post.topic_slug}/${post.topic_id}/${post.post_number}`;
          const avatarUrl = post.avatar_template.replace("{size}", "45");
          const excerpt = post.excerpt?.replace(/<\/?[^>]+(>|$)/g, "")?.slice(0, 120) + (post.excerpt?.length > 120 ? '...' : '') || '';

          const categoryHtml = category
            ? `<span style="font-size: 0.85em; color: #666;">카테고리: <strong>${category}</strong></span><br>`
            : '';

          const tagsHtml = tags.length
            ? `<span style="font-size: 0.85em; color: #666;">태그: ${tags.map(tag => `<span style="background:#eee; padding:2px 6px; border-radius:3px; margin-right:4px;">${tag}</span>`).join("")}</span>`
            : '';

          const animationClass = animate ? 'new-comment' : '';

          return `
            <tr class="topic-list-item ${animationClass}" data-post-id="${post.id}">
              <td class="main-link clearfix">
                <div style="display: flex; align-items: center; gap: 16px; padding: 8px 0;">
                  <div style="flex-shrink: 0;">
                    <a class="avatar-link" href="/u/${post.username}">
                      <img loading="lazy" width="45" height="45" src="${avatarUrl}" class="avatar" alt="${post.username}">
                    </a>
                  </div>
                  <div style="display: flex; flex-direction: column; justify-content: center; padding-top: 8px; padding-bottom: 8px;">
                    <span class="link-top-line" style="margin-bottom: 6px;">
                      <a href="${url}" class="title raw-link">${excerpt}</a>
                    </span>
                    <div class="link-bottom-line">
                      ${categoryHtml}
                      ${tagsHtml}
                    </div>
                  </div>
                </div>
              </td>
            </tr>
          `;
        }).join("");

        // 애니메이션 스타일이 아직 없으면 추가합니다.
        if (!document.getElementById('latest-replies-style')) {
          const style = document.createElement('style');
          style.id = 'latest-replies-style';
          style.textContent = `
            @keyframes highlightNew {
              0% { background-color: rgba(255, 255, 0, 0.3); }
              100% { background-color: transparent; }
            }
            .new-comment {
              animation: highlightNew 2s ease-out;
            }
          `;
          document.head.appendChild(style);
        }

        // 기존 컨테이너가 있으면 제거합니다.
        const existingContainer = document.querySelector(".latest-replies-container");
        if (existingContainer) {
          existingContainer.remove();
        }

        // 최근 댓글 섹션의 컨테이너를 생성합니다.
        const latestRepliesContainer = document.createElement("div");
        latestRepliesContainer.className = "latest-replies-container";
        latestRepliesContainer.style.marginTop = "2em";

        // 수동 새로고침 버튼과 상태 표시기를 추가합니다.
        const cacheTime = new Date(parseInt(localStorage.getItem(CACHE_TIMESTAMP_KEY) || Date.now()));
        const formattedTime = cacheTime.toLocaleTimeString();
        
        latestRepliesContainer.innerHTML = `
          <table class="topic-list latest-topic-list">
            <thead>
              <tr>
                <th class="default">
                  최근 댓글
                  <span id="comments-status" style="font-size: 0.8em; font-weight: normal; margin-left: 10px;">
                  </span>
                  <button id="refresh-comments" class="btn btn-flat no-text btn-icon" style="float: right;" title="댓글 새로고침">
                    <svg class="fa d-icon d-icon-sync svg-icon svg-string" width="16" height="16" aria-hidden="true"><use xlink:href="#sync"></use></svg>
                  </button>
                </th>
              </tr>
            </thead>
            <tbody id="latest-replies-tbody">
              ${rows}
            </tbody>
          </table>
        `;

        container.parentNode.insertBefore(latestRepliesContainer, container.nextSibling);
        container.dataset.modified = "true";
        
        // 새로고침 버튼에 클릭 이벤트를 추가합니다.
        document.getElementById("refresh-comments").addEventListener("click", function() {
          // 상태 텍스트를 업데이트합니다.
          const statusElement = document.getElementById("comments-status");
          if (statusElement) {
            statusElement.textContent = "(새로고침 중...)";
          }
          
          // 새로고침을 강제합니다.
          loadLatestReplies(false, true);
        });
        
        console.log(`댓글 ${results.length}개 렌더링 완료`);
      }
      
      // 초기 로딩을 시작합니다 (캐시 사용).
      loadLatestReplies(false, false);
      
      // 고빈도 폴링을 설정합니다.
      pollingIntervalId = setInterval(() => {
        // 사용자가 홈 페이지에 있을 때만 업데이트합니다.
        if (window.location.pathname === "/") {
          loadLatestReplies(true, false); // 조용한 모드, 사용 가능하면 캐시 사용
        }
      }, POLLING_INTERVAL);
      
      console.log(`폴링이 ${POLLING_INTERVAL}ms 간격으로 설정됨`);
      
      // 사용자가 페이지를 떠날 때 간격을 정리합니다.
      api.onPageChange((url) => {
        if (url !== "/") {
          console.log("홈 페이지에서 나감, 리소스 정리");
          
          if (pollingIntervalId) {
            clearInterval(pollingIntervalId);
            pollingIntervalId = null;
          }
          
          window.latestRepliesInitialized = false;
        }
      });
    }
  });
</script>

```

기대했던 대로 작동하고 있습니다. 새 댓글이 자동으로 추가되고, 그 후 캐시에 추가됩니다. 이것이 이상적인 방법은 아니라는 것을 알고 있습니다. 곧 플러그인을 만들어 보겠습니다.

여기서 목표는: 최신 토픽 페이지 스타일의 카테고리가 아니라, 최신 답변이 있는 카테고리를 만드는 것입니다.

---

<div class="post-metadata">

### Author: ![system](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/system/32/443519_2.png) [@system](https://meta.discourse.org/u/system)
#### Post date: [6월 6, 2025, 4:42오후 UTC](https://meta.discourse.org/t/recent-replies-takes-2-seconds-to-display/365041/8 "2025-06-06T16:42:48Z")

</div>

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.
