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

Contact Me

© 2026 SEOJing. All rights reserved.

ReactuseSyncExternalStoreHydrationlocalStorage트러블슈팅DevLog

React 하이드레이션 오류: localStorage 읽기를 useSyncExternalStore로 다룬 사례

2026년 3월 16일·수정 2026년 9월 30일·14분 읽기

문제 상황

블로그의 "최근 읽은 글"과 "포스트 탐색기" 컴포넌트에서 하이드레이션 에러가 터졌다.

원인은 서버에서는 빈 목록을 그렸는데, 클라이언트의 첫 렌더링에서는 localStorage에 저장된 글을 바로 읽어 다른 HTML을 만든 것이었다. 이 사례에서는 서버와 하이드레이션 중에 같은 빈 스냅샷을 쓰고, 하이드레이션이 끝난 뒤 브라우저의 값을 읽도록 바꿨다.

Uncaught Error: Hydration failed because the server rendered HTML
didn't match the client.
에러 메시지의 diff를 보면 원인이 명확하다.
// RecentlyRead 컴포넌트
+  <div className="flex gap-2 overflow-x-auto ...">   ← 클라이언트
-  <p className="text-sm text-gray-500 ...">            ← 서버

// PostExplorer의 FileItem 컴포넌트
+  className="... text-gray-400 dark:text-gray-500"   ← 클라이언트 (방문한 글)
-  className="... text-gray-800 dark:text-gray-200"   ← 서버 (미방문)

서버에서는 "읽은 글 없음" 상태로 렌더링되고, 클라이언트에서는 localStorage에 저장된 데이터로 렌더링된다. 두 결과가 다르니 React가 하이드레이션 에러를 던진 것이다.

localStorage가 React 하이드레이션 오류를 일으킨 이유

두 컴포넌트 모두 getReadPosts()를 호출해서 읽은 글 목록을 가져온다. 이 함수의 구현을 보자.

ts
export function getReadPosts(): ReadRecord[] {
  if (typeof window === "undefined") return [];
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    return raw ? (JSON.parse(raw) as ReadRecord[]) : [];
  } catch {
    return [];
  }
}

서버에서는 window가 없으므로 빈 배열을 반환한다. 클라이언트에서는 localStorage에서 데이터를 읽어 반환한다.

하이드레이션의 핵심 규칙은 이것이다 — 서버에서 렌더링한 HTML과 클라이언트의 첫 번째 렌더링 결과가 동일해야 한다.

서버는 빈 배열 → "열람한 페이지가 없습니다" <p> 태그를 렌더링하고, 클라이언트는 localStorage 데이터 → 카드 목록 <div>를 렌더링한다. HTML 구조 자체가 달라지므로 React가 에러를 던진다.

첫 번째 시도: useState + useEffect

흔히 알려진 해결법은 useState + useEffect 패턴이다.
tsx
export function RecentlyRead({ rootPath = "/" }: RecentlyReadProps) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  const readPosts = mounted ? getReadPosts() : [];
  // ...
}

마운트 전까지는 빈 배열을 사용하고, useEffect로 마운트 후에 mounted를 true로 바꿔서 localStorage 데이터를 읽는다. 서버와 클라이언트 첫 렌더링이 같아지므로 하이드레이션 에러는 사라진다.

하지만 이 구현에서는 동기적인 effect 내부 상태 변경을 지적하는 경고가 나왔다.

Error: Calling setState synchronously within an effect
can trigger cascading renders

이 경고는 모든 useState + useEffect 조합이 잘못됐다는 뜻은 아니다. 여기서는 외부 저장소를 읽기 위해 mounted 상태를 한 번 더 바꾸면서 추가 렌더링을 만들었다. React의 set-state-in-effect 규칙은 이런 동기적 상태 변경을 경고한다.

useSyncExternalStore란 무엇인가

useSyncExternalStore는 React 18에서 도입된 Hook으로, React 외부의 데이터 소스를 구독하기 위해 만들어졌다.

ts
const value = useSyncExternalStore(
  subscribe, // 외부 스토어가 변경될 때 호출될 콜백을 등록
  getSnapshot, // 클라이언트에서 현재 값을 가져오는 함수
  getServerSnapshot, // 서버에서 사용할 초기값을 반환하는 함수
);
세 가지 인자를 받는다.
  1. subscribe — 스토어가 변경될 때 React에게 알리는 구독 함수
  2. getSnapshot — 클라이언트에서 현재 값을 읽는 함수
  3. getServerSnapshot — SSR 시 사용할 값을 반환하는 함수

핵심은 세 번째 인자다. 서버 렌더링과 클라이언트의 하이드레이션 중 모두 getServerSnapshot을 사용한다. 두 시점의 초기 값이 같아야 HTML도 일치한다. 하이드레이션이 끝난 뒤 getSnapshot으로 브라우저의 값을 읽는다. React가 불일치를 허용하는 게 아니라, 첫 렌더링의 값을 맞춘 뒤 전환하는 것이다.

useState + useEffect로도 초기 값을 맞출 수 있다. 다만 이 사례에서는 localStorage를 React 외부의 데이터 소스로 구독해야 했기 때문에 useSyncExternalStore가 문제의 경계를 더 직접적으로 표현했다.

해결: useSyncExternalStore 적용

subscribe 함수

localStorage는 다른 탭에서 변경될 때 storage 이벤트를 발생시킨다. 이걸 subscribe 함수로 사용한다.

ts
function subscribeStorage(cb: () => void) {
  window.addEventListener("storage", cb);
  return () => window.removeEventListener("storage", cb);
}

storage 이벤트는 값을 쓴 현재 탭에서는 발생하지 않고, 같은 출처의 다른 탭 등에 전달된다. 따라서 이 구독 함수만으로 현재 탭의 모든 쓰기 직후 갱신을 보장하지는 않는다. 현재 탭에서도 즉시 반영해야 한다면 쓰기 함수에서 별도의 알림을 보내는 방식이 필요하다.

RecentlyRead 컴포넌트

tsx
const EMPTY_POSTS: ReadRecord[] = [];
const EMPTY_SET = new Set<string>();

export function RecentlyRead({ rootPath = "/" }: RecentlyReadProps) {
  const readPosts = useSyncExternalStore(
    subscribeStorage,
    getReadPosts, // 클라이언트: localStorage에서 읽기
    () => EMPTY_POSTS, // 서버: 빈 배열
  );
  const commentedPosts = useSyncExternalStore(
    subscribeStorage,
    getCommentedPosts,
    () => EMPTY_SET,
  );
  // ...
}

PostExplorer 컴포넌트

tsx
const EMPTY_POSTS: ReadRecord[] = [];

export function PostExplorer({ rootPath = "/" }: PostExplorerProps) {
  const readPosts = useSyncExternalStore(
    subscribeStorage,
    getReadPosts,
    () => EMPTY_POSTS,
  );
  const visitedHref = new Set(readPosts.map((p) => p.href));
  // ...
}

이 사례에서 useSyncExternalStore를 고른 이유

두 방식의 차이를 정리하면 이렇다.
[useState + useEffect]
  서버 렌더링 → HTML (빈 데이터)
  클라이언트 하이드레이션 → 첫 렌더링 (빈 데이터, 서버와 일치 ✓)
  useEffect 실행 → setMounted(true) → 리렌더링 ← cascading render 경고!
  두 번째 렌더링 → localStorage 데이터 표시

[useSyncExternalStore]
  서버 렌더링 → HTML (getServerSnapshot = 빈 데이터)
  클라이언트 하이드레이션 → getServerSnapshot = 빈 데이터 (서버와 일치 ✓)
  하이드레이션 후 → getSnapshot = localStorage 데이터
  외부 스토어가 달라졌으면 다시 렌더링

이 방식도 저장된 값이 다르면 하이드레이션 후 화면을 다시 그린다. 렌더링이 한 번으로 끝나거나 빈 상태가 전혀 보이지 않는다고 약속할 수는 없다. 선택 이유는 렌더링 횟수 자체보다 외부 스토어의 스냅샷과 구독을 React에 명시하는 데 있다.

주의할 점 — getSnapshot은 반드시 캐싱해야 한다

getSnapshot 함수는 호출될 때마다 동일한 참조를 반환하거나, 값이 실제로 변경되었을 때만 새 객체를 반환해야 한다. 매 호출마다 새 객체를 반환하면 React가 무한 리렌더링을 일으킨다.

The result of getSnapshot should be cached to avoid an infinite loop

실제로 이 에러가 터졌다. getReadPosts()는 JSON.parse()로 매번 새 배열을 생성하고, getCommentedPosts()는 매번 new Set()을 생성한다. useSyncExternalStore는 Object.is()로 이전 스냅샷과 새 스냅샷을 비교하는데, 매번 새 참조이므로 항상 "변경됨"으로 판단하고 리렌더링을 트리거한다. 리렌더링 → getSnapshot 호출 → 새 참조 → 리렌더링 → 무한 루프.

해결법은 모듈 레벨 캐시를 두고, localStorage의 raw 문자열이 변경되었을 때만 새 객체를 생성하는 것이다.

ts
const EMPTY_READ_POSTS: ReadRecord[] = [];
let _readPostsCache: ReadRecord[] = EMPTY_READ_POSTS;
let _readPostsRaw: string | null = null;

export function getReadPosts(): ReadRecord[] {
  if (typeof window === "undefined") return EMPTY_READ_POSTS;
  try {
    const raw = localStorage.getItem(STORAGE_KEY);
    if (raw !== _readPostsRaw) {
      _readPostsRaw = raw;
      _readPostsCache = raw ? (JSON.parse(raw) as ReadRecord[]) : [];
    }
    return _readPostsCache;
  } catch {
    return EMPTY_READ_POSTS;
  }
}

raw 문자열이 이전과 같으면 _readPostsCache를 그대로 반환한다. 같은 참조이므로 Object.is()가 true를 반환하고, React는 리렌더링을 건너뛴다. getCommentedPosts()도 동일한 패턴으로 캐싱한다. 저장소를 읽을 수 없는 경우에도 매번 새 배열이나 Set을 만들지 않고 안정적인 빈 스냅샷을 반환해야 한다.

서버 스냅샷으로 사용하는 빈 배열과 빈 Set은 모듈 레벨 상수로 선언한다. 컴포넌트 안에서 () => []를 쓰면 호출할 때마다 새 참조가 생긴다. 서버와 하이드레이션에서 같은 초기 값을 나타내도록 안정적인 참조를 쓰는 편이 낫다.

ts
// ✓ 모듈 레벨 상수 — 참조가 안정적
const EMPTY_POSTS: ReadRecord[] = [];
const EMPTY_SET = new Set<string>();

// △ 컴포넌트 안에서 인라인 — 호출할 때마다 새 참조
useSyncExternalStore(subscribe, getSnapshot, () => []);

핵심 교훈

브라우저 전용 값을 읽는 모든 컴포넌트에 useSyncExternalStore가 필수인 것은 아니다. 이 사례처럼 컴포넌트 밖에서 바뀌는 값을 구독해야 할 때 적합하다. 컴포넌트 자체 상태로 충분하다면 useState도 선택지다.

이 Hook이 해결하는 문제는 두 가지다.

  1. 하이드레이션 불일치 방지 — 서버와 클라이언트 초기 렌더링에서 같은 getServerSnapshot 사용
  2. 외부 값 구독 — 저장소 변경 알림과 스냅샷 읽기를 연결

typeof window !== "undefined" 분기로 서버/클라이언트를 나누는 건 React가 의도를 모르는 상태에서 첫 렌더링 결과를 달라지게 할 수 있다. useSyncExternalStore는 "이 데이터는 외부 스토어에서 온다"는 경계를 React에 알려준다. 그래도 일치하는 서버 스냅샷, 안정적인 클라이언트 스냅샷, 변경 알림은 직접 올바르게 구현해야 한다.

참고자료

  • React: useSyncExternalStore
  • React: set-state-in-effect 규칙
  • MDN: storage 이벤트

Post Q&A

오케이징에게 물어보기

React 하이드레이션 오류: localStorage 읽기를 useSyncExternalStore로 다룬 사례 전체를 기준으로 질문과 피드백을 받아요.답을 본 뒤에는 이 내용을 댓글로 달아서 서징에게도 물어볼 수 있어요. 작성자가 직접 볼 수 있어요!

0/500

포스트 목록

/SEOJing
파일 15개, 폴더 1개
Cloudflare Workers에서 fs 모듈이 안 되는 이유와 해결법대표 이미지 자동화 실험 — 검색과 Codex 생성이 같은 경로로 붙었다본문 이미지를 나중에 넣는 게 아니라 — SEOJing 글쓰기 파이프라인에 시각 판단을 넣기모바일 웹에서 가로 모드를 강제하는 5가지 방법 — iOS Safari에서도 동작하는 코드 뷰어 만들기블로그 글을 PPT로 만들기 — DOM 클로닝 기반 프레젠테이션 모드100vh가 100%가 아닌 이유 — 모바일 뷰포트 단위 완전 정리Context로 퀴즈 컴포넌트를 만들다 막혀서 React.Children을 공부하게 된 이야기대표 이미지를 글마다 다시 붙이는 방식 — 사진 검색에서 리소그래프 배경까지글 위에 영상을 붙인다는 것 — SEOJing 요약 쇼츠 파이프라인React 하이드레이션 오류: localStorage 읽기를 useSyncExternalStore로 다룬 사례useEffect cleanup과 의존성 배열 — 실전 버그 사례로 이해하는 생애주기vinext + GitHub Actions로 Cloudflare Workers 배포하기vinext 오픈소스 기여기: 한국어 slug가 RSC에서 이슈를 일으킨 이유RSC 환경에서 WebAssembly가 차단되는 이유 — Shiki에서 rehype-prism-plus로vinext는 왜 빠를까? — SSR, Vite, Edge, 그리고 Web Vitals까지

같은 섹션의 대표 이미지

39 posts · latest first
본문 이미지를 나중에 넣는 게 아니라 — SEOJing 글쓰기 파이프라인에 시각 판단을 넣기 글의 대표 이미지
SEO Jing26. 06. 22.

본문 이미지를 나중에 넣는 게 아니라 — SEOJing 글쓰기.

SEOJing에서 새 글을 쓸 때 대표 이미지와 본문 이미지를 빼먹지 않도록, 블로그 맵과 글쓰기 파이프라인 안에 시각 판단 단계를 넣은 과정을 정리했다.

26. 06. 22.SEOJing
SEO Jing26. 06. 22.

글 위에 영상을 붙인다는 것 — SEOJing 요약.

SEOJing 글을 소셜용 영상으로 따로 소비시키는 게 아니라, 포스트 상단 요약과 블로그 유입 장치로 연결하기 위해 summaryVideo frontmatter와 Supertonic3 기반 요약 쇼츠 파이프라인을 붙인 과정을 정리했다.

26. 06. 22.SEOJing
대표 이미지 자동화 실험 — 검색과 Codex 생성이 같은 경로로 붙었다 글의 리소그래프 스타일 대표 이미지 배경
SEO Jing26. 06. 21.

대표 이미지 자동화 실험 — 검색과 Codex 생성이 같은.

SEOJing 블로그에 대표 이미지를 자동으로 붙이는 실험을 실제로 돌려봤다. 검색 기반 cover 삽입과 Codex CLI 기반 정적 SVG 생성이 같은 frontmatter 경로로 연결됐다.

26. 06. 21.SEOJing
대표 이미지를 글마다 다시 붙이는 방식 글의 리소그래프 스타일 대표 이미지 배경
SEO Jing26. 06. 21.

대표 이미지를 글마다 다시 붙이는 방식 — 사진 검색에서.

SEOJing 포스트 목록을 파일 탐색기처럼만 두지 않고, 최신 글부터 실제 사진 기반 리소그래프 배경을 붙이는 실험을 정리합니다. 이미지는 배경만 만들고, 제목과 아이콘은 블로그 UI가 맡는 쪽으로 방향을 바꿨습니다.

26. 06. 21.SEOJing
SEO Jing26. 04. 01.

Day 12 - 테스트 커버리지 개선, 모바일 프레젠테이션 버그 2건.

SEO Jing 개발 열두째 날. code-block 테스트 14개 추가로 커버리지 대폭 개선, 모바일 프레젠테이션에서 FullscreenView 방향 전환 문제와 스크롤 멈춤 버그 수정.

26. 04. 01.SEOJing
SEO Jing26. 04. 01.

useEffect cleanup과 의존성 배열 — 실전 버그.

useEffect 의존성 배열에 불필요한 값이 포함되면 cleanup과 재실행이 뒤엉켜 DOM 상태가 꼬일 수 있다. 프레젠테이션 모드에서 발생한 모바일 스크롤 고착 버그를 통해 원인과 해결 패턴을 정리한다.

26. 04. 01.SEOJing
SEO Jing26. 03. 25.

Day 11 - 프레젠테이션 확대 기능,.

SEO Jing 개발 열한째 날. PC 프레젠테이션 확대/축소 컨트롤 추가, 모바일 orientation 판단 로직 개선, FullscreenView를 독립 컴포넌트로 분리 및 PC 대응.

26. 03. 25.SEOJing
SEO Jing26. 03. 25.

vinext 오픈소스 기여기: 한국어 slug가 RSC에서.

한국어 MDX 블로그를 만들다 vinext 프레임워크의 ByteString 버그를 발견하고, 이슈를 작성하고, PR을 올리기까지의 과정

26. 03. 25.SEOJing
SEO Jing26. 03. 25.

vinext는 왜 빠를까? — SSR, Vite, Edge,.

vinext가 빠른 이유를 이해하기 위해, SSR부터 Hydration, 빌드 도구, Edge Runtime, Web Vitals, RSC, CDN 캐싱, ISR, PPR까지 웹 렌더링 성능의 전체 그림을 정리한다

26. 03. 25.SEOJing
SEO Jing26. 03. 24.

100vh가 100%가 아닌 이유 — 모바일 뷰포트 단위 완전 정리.

모바일 Safari에서 100vh가 화면을 넘치는 이유, vh/svh/lvh/dvh의 차이, JavaScript에서 실제 뷰포트를 구하는 방법, 그리고 전체화면 UI를 만들 때 알아야 할 CSS zoom과 모바일 판정 패턴까지 정리한다.

26. 03. 24.SEOJing
SEO Jing26. 03. 23.

Day 10 - 프레젠테이션 모드 안정화.

SEO Jing 개발 열째 날. 프레젠테이션 모드의 모바일 UX 문제들을 전면 수정. 퀴즈·코드블록·이미지·포스트목록 처리 개선, 롱프레스 UX 및 하단 바 레이아웃 안정화. 모바일 뷰포트·리스트 분할 문제 수정, 채움 비율 보수적으로 조정, 포스트 탐색기 자연 정렬 적용.

26. 03. 23.SEOJing
SEO Jing26. 03. 22.

Day 9 - 프레젠테이션 모드, 코드블럭 개선, 테스팅.

SEO Jing 개발 아홉째 날. 프레젠테이션 기능 추가, 퀴즈 구조 변경, 모바일 반응형, 코드블럭 사용성, 테스팅 도입.

26. 03. 22.SEOJing
SEO Jing26. 03. 22.

모바일 웹에서 가로 모드를 강제하는 5가지 방법 — iOS.

모바일 웹에서 코드 블록을 가로로 넓게 보여주고 싶었다. screen.orientation.lock()은 iOS에서 안 되고, PWA manifest는 브라우저에서 무시된다. 결국 CSS transform으로 가짜 회전을 만들었고, 그 과정에서 엄지 접근성까지 고민하게 됐다.

26. 03. 22.SEOJing
SEO Jing26. 03. 22.

블로그 글을 PPT로 만들기 — DOM 클로닝 기반.

MDX 파일을 수정하지 않고, 렌더된 DOM을 h2 기준으로 자르고 화면 높이에 맞춰 자동 페이지네이션하는 프레젠테이션 모드를 만들었다. 리스트 높이 측정이 왜 틀리는지 디버깅한 과정과, ul/ol을 li 단위로 분할하는 해결책을 정리한다.

26. 03. 22.SEOJing
SEO Jing26. 03. 21.

Day 8 - 아티클 퀴즈와 스터디 자료.

SEO Jing 개발 여덟째 날. 아티클 퀴즈 디자인시스템 구현과 스터디 대면 자료 작성.

26. 03. 21.SEOJing
SEO Jing26. 03. 21.

Context로 퀴즈 컴포넌트를 만들다 막혀서.

MDX 블로그에 퀴즈 컴포넌트를 만들면서, Context 기반 Compound Component로 시작했다가 index 문제에 막혀 React.Children API를 채택하게 된 과정을 정리한다.

26. 03. 21.SEOJing
SEO Jing26. 03. 20.

Day 7 - Front Matter CMS와 관련 게시물.

SEO Jing 개발 일곱째 날. Front Matter CMS 설치와 관련 게시물 이동 탐색기 구현.

26. 03. 20.SEOJing
SEO Jing26. 03. 18.

Day 6 - 스터디 자료 작성과 데스크탑 비율 수정.

SEO Jing 개발 여섯째 날. 데스크탑 비율 수정과 씨랩 스터디 사전 진단 자료 작성.

26. 03. 18.SEOJing
SEO Jing26. 03. 17.

Day 5 - shiki 제거, MDX 모듈화, 그리고.

SEO Jing 개발 다섯째 날. shiki를 rehype-prism-plus로 교체하고, gray-matter 직접 구현, MDX 모듈화, 페이지 내 검색, 테이블 디자인시스템까지.

26. 03. 17.SEOJing
SEO Jing26. 03. 16.

Day 4 - 배포와 CI/CD.

SEO Jing 개발 넷째 날. lint, codecov, Cloudflare 배포, fs 런타임 이슈.

26. 03. 16.SEOJing
SEO Jing26. 03. 16.

Cloudflare Workers에서 fs 모듈이 안 되는 이유와.

배포 후 블로그 포스트가 404를 반환하던 문제부터, gray-matter eval 차단, next-mdx-remote eval 차단까지 — 세 겹으로 터진 이슈를 하나씩 해결한 기록

26. 03. 16.SEOJing
SEO Jing26. 03. 16.

React 하이드레이션 오류: localStorage.

React에서 localStorage를 첫 렌더링 중 읽을 때 생기는 하이드레이션 오류를 실제 블로그 사례로 설명하고, getServerSnapshot과 안정적인 스냅샷으로 해결한 과정을 정리한다.

26. 03. 16.SEOJing
SEO Jing26. 03. 16.

vinext + GitHub Actions로.

vinext 프로젝트를 GitHub Actions로 Cloudflare Workers에 자동 배포하는 방법과 실제 겪은 트러블슈팅 기록

26. 03. 16.SEOJing
SEO Jing26. 03. 16.

RSC 환경에서 WebAssembly가 차단되는 이유 —.

코드 하이라이팅에 Shiki를 쓰면 왜 RSC에서 WebAssembly.instantiate() 에러가 터지는지, 그리고 빌드 타임 하이라이팅으로 어떻게 해결했는지 정리한다.

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

엄청난 피드백.

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

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

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

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

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

MDX 관련 이슈 노트.

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

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

Day 3 - MDX 이슈, 반응형, 다크모드.

SEO Jing 개발 셋째 날. 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. 14.

Day 2 - 블로그 스켈레톤과 MDX 파싱.

SEO Jing 개발 둘째 날. 디자인 시스템 확장, 블로그 스켈레톤, 폰트 이슈 해결.

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
SEO Jing26. 03. 13.

Day 1 - 디자인 컨셉과 디자인 시스템.

SEO Jing 개발 첫째 날. 디자인 컨셉 설정과 디자인 시스템 구축을 시작했다.

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

기술 블로그를 직접 제작하게 된 이유.

SEO Jing을 개발하게 된 이유입니다.

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

왜 자꾸 프로젝트가 중단되는지.

프로젝트가 중단되는 이유에 대한 자기 회고입니다.

26. 03. 13.SEOJing
React 하이드레이션 오류: localStorage 읽기를 useSyncExternalStore로 다룬 사례 | SEOJing