LogoSEO Jing
  • All Posts
  • SEO Jing
  • okayJing
  • KD Team
  • CLAB Coreteam
  • Study

Contact Me

© 2026 SEOJing. All rights reserved.

DevLogSEO JingInsight

생각보다 어려웠던 댓글, 완독 로컬스토리지

2026년 3월 15일·7분 읽기

왜 만들었나

스터디원에게 과제를 내준다면 어떻게 피드백을 할지가 고민이었다. 내가 이번주 과제를 했나? 내가 읽었나? 어디까지 읽었나?를 피드백 해주고 싶었다.

그래서 네 가지 기능을 한꺼번에 만들기로 했다.
  1. 글을 열면 자동으로 "최근 읽은 글"에 기록
  2. 포스트 목록에서 읽었던 글은 회색으로 표시
  3. 스크롤 진행률을 추적해서 프로그레스 바로 보여주기
  4. 다 읽고 댓글까지 남기면 우표 도장 찍기

글 읽기 자동 추적

프로젝트에는 이미 markAsRead() 함수가 있었다. localStorage에 읽은 글 목록을 저장하는 함수인데, 문제는 아무 데서도 호출하지 않고 있었다는 것.

블로그 글 페이지(page.tsx)는 서버 컴포넌트다. localStorage는 클라이언트에서만 접근 가능하니 여기서 직접 호출할 수 없다. 글 하단에 이미 ArticleToolbar라는 클라이언트 컴포넌트가 있었고, slug 정보도 이미 갖고 있었다. 여기에 title prop만 추가하고 useEffect에서 markAsRead를 호출하면 끝이었다.

tsx
useEffect(() => {
  markAsRead(`/blog/${slug}`, title);
}, [slug, title]);

새 컴포넌트를 만들지 않고 기존 것을 활용한 이유는 단순하다. 불필요한 클라이언트 바운더리를 늘리면 SSR 이점이 줄어들고, 이미 적절한 위치에 적절한 컴포넌트가 있었으니까.


포스트 목록에서 읽은 글 회색 처리

파일 탐색기 형태의 PostExplorer에서 읽었던 글의 제목을 회색으로 보여주고 싶었다. 브라우저가 방문한 링크를 보라색으로 바꾸는 것과 같은 맥락이다.

tsx
<span
  className={cn(
    "flex-1 text-left font-sans truncate",
    file.visited
      ? "text-gray-400 dark:text-gray-500"
      : "text-gray-800 dark:text-gray-200",
  )}
>
  {file.name}
</span>

스크롤 진행률 추적

기존 ReadRecord에 progress?: number 필드 하나만 추가했다. 0~100 사이의 정수.

tsx
useEffect(() => {
  const article = toolbarRef.current?.closest("section");
  if (!article) return;

  let ticking = false;
  const handleScroll = () => {
    if (ticking) return;
    ticking = true;
    requestAnimationFrame(() => {
      const rect = article.getBoundingClientRect();
      const scrolled = -rect

requestAnimationFrame으로 throttle하는 이유는 scroll 이벤트가 초당 수십 번 발생하기 때문이다. updateReadProgress는 Math.max로 최대값만 유지한다. 스크롤을 올려도 진행률이 줄어들지 않는다.


댓글 추적과 우표 도장

댓글 감지의 어려움

이 프로젝트는 Giscus를 사용한다. GitHub Discussions 기반의 댓글 시스템인데, iframe으로 동작해서 부모 페이지에서 직접 댓글 작성 여부를 알기 어렵다.

Giscus는 postMessage API로 부모와 통신하는데, 처음에는 emitMetadata="0"으로 설정되어 있어서 discussion 메타데이터 자체가 넘어오지 않았다. 이걸 "1"로 바꿔야 totalCommentCount 같은 정보를 받을 수 있었다.

첫 번째 실수: discussion 로드를 댓글로 오인

처음 구현할 때는 data?.discussion이 존재하면 onComment()를 호출했다. 문제는 댓글을 안 달아도 discussion이 처음 로드될 때 이 이벤트가 온다는 것. 모든 글을 열기만 해도 "댓글 완료"로 찍히는 버그가 생겼다.

해결: 댓글 수 비교

tsx
const prevCommentCountRef = useRef<number | null>(null);

const handleMessage = useCallback(
  (event: MessageEvent) => {
    if (event.origin !== "https://giscus.app") return;
    const data = event.data?.giscus;
    if (data?.discussion) {
      const totalCommentCount = data.discussion.totalCommentCount;
      if (totalCommentCount != null) {
        if 

첫 번째 메시지에서는 prevCommentCountRef.current가 null이므로 콜백이 호출되지 않고, 두 번째부터 비교가 시작된다.


정리

localStorage만으로 꽤 풍부한 읽기 추적 경험을 만들 수 있었다. 서버 없이, 로그인 없이, 브라우저 안에서 완결되는 기능이라 구현이 가볍고 프라이버시 문제도 없다.

개발하면서 느낀 점은, 기존 코드를 잘 활용하면 새로 만들 게 생각보다 적다는 것. markAsRead는 이미 있었고, ArticleToolbar도 이미 있었고, Giscus의 postMessage도 이미 리스닝하고 있었다. 연결만 해주면 됐다.

Post Q&A

오케이징에게 물어보기

생각보다 어려웠던 댓글, 완독 로컬스토리지 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!

0/500

포스트 목록

/SEOJing/devLog/insight
파일 10개, 폴더 0개
엄청난 피드백생각보다 어려웠던 댓글, 완독 로컬스토리지디자인 시스템을 구축할 때 주의할 점폰트는 왜 메인 페이지에서만 적용이 안되고 있었을까?MDX DOM 트리 파싱하기MDX 관련 이슈 노트결국 Node.js 까지 와버렸다전체적인 플로우Storybook으로 디자인 시스템 테스팅하기MDX가 뭘까?

같은 섹션의 대표 이미지

10 posts · latest first
SEO Jing26. 03. 15.

엄청난 피드백.

CLI 코드 리뷰에서 받은 피드백과 전체 코드 수정 계획을 정리했다.

26. 03. 15.SEOJing
.
top
;
const total = rect.height - window.innerHeight;
if (total > 0) {
const progress = Math.min(
100,
Math.max(0, Math.round((scrolled / total) * 100)),
);
updateReadProgress(href, progress);
}
ticking = false;
});
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [slug]);
(
prevCommentCountRef.current !== null &&
totalCommentCount > prevCommentCountRef.current
) {
onComment?.();
}
prevCommentCountRef.current = totalCommentCount;
}
}
},
[onComment],
);
SEO Jing26. 03. 15.

생각보다 어려웠던 댓글, 완독 로컬스토리지.

localStorage만으로 글 읽기 추적, 스크롤 진행률, 댓글 감지를 구현한 과정을 정리했다.

26. 03. 15.SEOJing
SEO Jing26. 03. 15.

MDX 관련 이슈 노트.

블로그 디테일 페이지에서 MDX를 렌더링하기 위해 검토한 라이브러리들과 최종 선택 과정.

26. 03. 15.SEOJing
SEO Jing26. 03. 14.

디자인 시스템을 구축할 때 주의할 점.

디자인 시스템 구현 시 파일 구조, 디자인 토큰, 유의 사항을 정리했다.

26. 03. 14.SEOJing
SEO Jing26. 03. 14.

폰트는 왜 메인 페이지에서만 적용이 안되고 있었을까?.

Tailwind v4 환경에서 폰트가 메인 페이지에서만 적용되지 않던 원인과 Hydration Mismatch 이슈를 정리했다.

26. 03. 14.SEOJing
SEO Jing26. 03. 14.

MDX DOM 트리 파싱하기.

MDX 파일의 경로 탐색 로직과 콘텐츠 트리 생성 과정을 정리했다.

26. 03. 14.SEOJing
SEO Jing26. 03. 14.

결국 Node.js 까지 와버렸다.

MDX 파일 구조를 JSON으로 변환하기 위해 Node.js의 fs 모듈을 배워봤다.

26. 03. 14.SEOJing
SEO Jing26. 03. 13.

전체적인 플로우.

SEO Jing 프로젝트의 기술 스택 선정과 전체적인 개발 플로우 정리.

26. 03. 13.SEOJing
SEO Jing26. 03. 13.

Storybook으로 디자인 시스템 테스팅하기.

Storybook의 사용법과 디자인 시스템 개발에서의 장점을 정리했다.

26. 03. 13.SEOJing
SEO Jing26. 03. 13.

MDX가 뭘까?.

MDX의 개념과 블로그에서 활용하는 이유를 정리했다.

26. 03. 13.SEOJing