From e34c4fe52bbd2fc8b41cbae7be5011ad5ffd7da5 Mon Sep 17 00:00:00 2001 From: choiboa Date: Sat, 31 Jan 2026 03:52:40 +0900 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8=20feat:=20=20=ED=91=9C&=ED=86=A0?= =?UTF-8?q?=EA=B8=80=20mdx=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MDXContent.tsx | 2 + src/components/mdx/Quote.tsx | 23 +++----- src/components/mdx/Table.tsx | 65 ++++++++++++++++++++++ src/components/mdx/Toggle.tsx | 100 ++++++++++++++++++++++++++++++++++ src/components/mdx/index.ts | 1 + src/styles/mdx.css | 10 ++++ 6 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 src/components/mdx/Table.tsx create mode 100644 src/components/mdx/Toggle.tsx diff --git a/src/components/MDXContent.tsx b/src/components/MDXContent.tsx index e0aedbb..5180317 100644 --- a/src/components/MDXContent.tsx +++ b/src/components/MDXContent.tsx @@ -1,4 +1,5 @@ import CodeBlockFigure from "@/components/mdx/CodeBlock"; +import { TableRoot } from "@/components/mdx/Table"; import type { ComponentType, ReactElement } from "react"; import * as runtime from "react/jsx-runtime"; @@ -10,6 +11,7 @@ type MDXModule = { const defaultMdxComponents: MDXComponents = { figure: CodeBlockFigure as ComponentType>, + table: TableRoot as ComponentType>, }; function isMDXModule(value: unknown): value is MDXModule { diff --git a/src/components/mdx/Quote.tsx b/src/components/mdx/Quote.tsx index ffeed9b..00320cc 100644 --- a/src/components/mdx/Quote.tsx +++ b/src/components/mdx/Quote.tsx @@ -9,27 +9,32 @@ const tonePreset: Record< bar: string; text: string; bg: string; + marker: string; } > = { black: { bar: "bg-neutral-400", text: "text-gray-700 dark:text-neutral-100/90", bg: "dark:bg-neutral-300/10", + marker: "marker:text-neutral-500 dark:marker:text-white/70", }, green: { bar: "bg-emerald-400", text: "text-gray-800 dark:text-emerald-50", bg: "bg-gray-100 dark:bg-gray-500/10", + marker: "marker:text-emerald-500 dark:marker:text-white/70", }, blue: { bar: "bg-blue dark:bg-muted", text: "text-[#0a1f47] dark:text-sky-50", bg: "bg-blue-200/10 dark:bg-sky-950/40", + marker: "marker:text-sky-600/50 dark:marker:text-white/70", }, pink: { bar: "bg-accent", text: "text-[#4f0e32] dark:text-white", bg: "bg-accent/5 dark:bg-accent/10", + marker: "marker:text-accent dark:marker:text-white/70", }, }; @@ -49,24 +54,14 @@ export default function Quote({ className={clsx( "border-0 pl-0 ml-0 not-italic", "my-6", - "font-normal text-sm " + "font-normal text-sm ", + "quotes-none before:content-none after:content-none" )} >
-
- +
{children}
diff --git a/src/components/mdx/Table.tsx b/src/components/mdx/Table.tsx new file mode 100644 index 0000000..19c51e8 --- /dev/null +++ b/src/components/mdx/Table.tsx @@ -0,0 +1,65 @@ +import clsx from "clsx"; +import type { ReactNode, TableHTMLAttributes } from "react"; + +export type TableProps = { + caption?: ReactNode; + className?: string; + children: ReactNode; +}; + +function tableInnerClass() { + return clsx( + "w-full text-sm border-separate border-spacing-0 rounded-md overflow-hidden", + + "border border-neutral-300 dark:border-white/15", + + " [&_thead_th]:bg-neutral-100 dark:[&_thead_th]:bg-white/5", + " [&_thead_th]:font-semibold", + " [&_thead_th]:text-left", + " [&_thead_th]:px-3 [&_thead_th]:py-2", + " [&_thead_th]:border-b", + " [&_thead_th]:border-neutral-300 dark:[&_thead_th]:border-white/15", + + " [&_tbody_td]:px-3 [&_tbody_td]:py-2", + " [&_tbody_td]:border-b", + " [&_tbody_td]:border-neutral-200 dark:[&_tbody_td]:border-white/10", + + " [&_th+th]:border-l [&_th+th]:border-neutral-200 dark:[&_th+th]:border-white/10", + " [&_td+td]:border-l [&_td+td]:border-neutral-200 dark:[&_td+td]:border-white/10", + + " [&_tbody_tr:last-child_td]:border-b-0", + + " [&_tbody_tr:hover]:bg-neutral-50 dark:[&_tbody_tr:hover]:bg-white/5", + + " [&_code]:bg-neutral-100 dark:[&_code]:bg-white/10", + " [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded" + ); +} + +export default function Table({ caption, className, children }: TableProps) { + return ( +
+ + {caption ? ( + + ) : null} + {children} +
+ {caption} +
+
+ ); +} + + +export type TableRootProps = TableHTMLAttributes & { + caption?: ReactNode; +}; + +export function TableRoot({ caption, className, children }: TableRootProps) { + return ( + + {children} +
+ ); +} diff --git a/src/components/mdx/Toggle.tsx b/src/components/mdx/Toggle.tsx new file mode 100644 index 0000000..22b93a7 --- /dev/null +++ b/src/components/mdx/Toggle.tsx @@ -0,0 +1,100 @@ +import clsx from "clsx"; +import { ChevronDown, Info } from "lucide-react"; +import type { HTMLAttributes, ReactNode } from "react"; + +type ToggleTone = "default" | "blue"; + +const tonePreset: Record< + ToggleTone, + { border: string; hover: string; text: string; summaryBg: string } +> = { + default: { + border: "border-neutral-200 dark:border-white/10", + hover: "hover:bg-neutral-50/80 dark:hover:bg-white/[0.04]", + text: "text-neutral-900/90 dark:text-white/90", + summaryBg: "bg-transparent", + }, + blue: { + border: "border-sky-200/70 dark:border-white/10", + hover: "hover:bg-sky-50/70 dark:hover:bg-white/[0.04]", + text: "text-[#0a1f47]/90 dark:text-white/90", + summaryBg: "bg-sky-50/30 dark:bg-sky-400/5", + }, +}; + +export default function Toggle({ + title, + children, + tone = "default", + defaultOpen = false, + dense = false, + className, + ...rest +}: { + title: ReactNode; + children: ReactNode; + tone?: ToggleTone; + defaultOpen?: boolean; + dense?: boolean; +} & HTMLAttributes) { + const t = tonePreset[tone]; + + return ( +
+ +
+
+ + + + +
+ {title} +
+
+ + + +
+
+ +
+
+ {children} +
+
+
+ ); +} diff --git a/src/components/mdx/index.ts b/src/components/mdx/index.ts index 78a34bc..1b912d5 100644 --- a/src/components/mdx/index.ts +++ b/src/components/mdx/index.ts @@ -4,3 +4,4 @@ export { default as Figure } from "./Figure"; export { default as LinkBadge } from "./LinkBadge"; export { default as PostMention } from "./PostMention"; export { default as Quote } from "./Quote"; +export { default as Toggle } from "./Toggle"; diff --git a/src/styles/mdx.css b/src/styles/mdx.css index 68fa566..1324a49 100644 --- a/src/styles/mdx.css +++ b/src/styles/mdx.css @@ -110,4 +110,14 @@ Inline code .prose a.no-underline { text-decoration: none !important; +} + +/* 인용 따옴표 제거 */ +.prose blockquote p::before, +.prose blockquote p::after { + content: none !important; +} + +details[open] summary .group-open\:rotate-180 { + transform: rotate(180deg); } \ No newline at end of file From da4b5170b577360083c553ef1cd7d750af410af3 Mon Sep 17 00:00:00 2001 From: choiboa Date: Sat, 31 Jan 2026 04:14:55 +0900 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=93=8E=20post=20:=20=EC=8B=9C?= =?UTF-8?q?=EB=A6=AC=EC=A6=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/posts/registry/series.registry.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/lib/posts/registry/series.registry.ts b/src/lib/posts/registry/series.registry.ts index d134bcc..e6fb337 100644 --- a/src/lib/posts/registry/series.registry.ts +++ b/src/lib/posts/registry/series.registry.ts @@ -50,10 +50,18 @@ export const SERIES_META_BY_CATEGORY: Record = { id: "nextjs", name: "Next.js", description: - "Next.js 기반 애플리케이션 구조, 렌더링 방식, 데이터 흐름을 고민하며 정리한 인사이트 모음", + "Next.js 기반 애플리케이션 구조, 렌더링 방식, 데이터 흐름을 고민하며 정리한 인사이트 모음", category: "Insight", tone: "gray", }, + { + id: "3d", + name: "3D", + description: + "3D 그래픽스의 원리, 렌더링 구조, 인터랙션과 성능을 폭넓게 탐구하며 정리한 인사이트 기록", + category: "Insight", + tone: "purple", + }, { id: "testing", name: "Test", From bf7bff300aeb278f415bf306036fd93cd0fffec6 Mon Sep 17 00:00:00 2001 From: choiboa Date: Sat, 31 Jan 2026 04:15:35 +0900 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=93=8E=20post=20:=20React/3d=20?= =?UTF-8?q?=EB=A0=8C=EB=8D=94=EB=A7=81=20=EC=B0=A8=EC=9D=B4=20=EB=B0=8F=20?= =?UTF-8?q?r3f=EC=97=90=20=EB=8C=80=ED=95=9C=20=EA=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/posts/DEV_LOG/roome-2-3dmodel.mdx | 2 +- content/posts/INSIGHT/r3f-renderingcycle.mdx | 439 +++++++++++++++++++ 2 files changed, 440 insertions(+), 1 deletion(-) create mode 100644 content/posts/INSIGHT/r3f-renderingcycle.mdx diff --git a/content/posts/DEV_LOG/roome-2-3dmodel.mdx b/content/posts/DEV_LOG/roome-2-3dmodel.mdx index f409789..12f7622 100644 --- a/content/posts/DEV_LOG/roome-2-3dmodel.mdx +++ b/content/posts/DEV_LOG/roome-2-3dmodel.mdx @@ -210,7 +210,7 @@ const { items } = useRoomItems({ roomId, furnitures });
+ + 3D + React 프로젝트를 진행하면서 가장 크게 느낀 점은 +
**UX 관점에서도, 성능 관점에서도 이 렌더링 사이클 차이를 이해**하는 것이 필수적이라는 점이었다. +
+ ### React와 3D 엔진은 렌더링을 바라보는 철학 자체가 다르다. + - React는 **상태 변화 중심** + - 3D 엔진은 **프레임 중심** + + + 이 차이를 이해하지 못하면 이런 문제가 생긴다. + 1. **병목(성능 문제)** — 렌더링 구조 차이에서 발생 + 2. **불일치(UX 문제)** — 타이밍 차이에서 발생 + + + **이 글에서는 그 차이가 어떠한 문제점들을 만드는지, 왜 R3F가 필요한지 정리**해보려 한다. + + + +--- + +## 1. React 렌더링 사이클 이해하기 + +- React는 기본적으로 **상태 변화가 렌더링을 유발하는 구조**이다. +- 그래서 기본적인 렌더링 사이클의 핵심은 "상태가 바뀌면 UI를 다시 계산한다"는 점이다. + + +
+ +### 🎯 React의 핵심 목표는 +- 최소한의 변경만 DOM에 반영 +- 불필요한 연산 줄이기 +- 선언적인 UI 관리 + +여기서 가장 중요한 점은 : React는 "필요할 때만" 화면을 업데이트 한다. +
즉, React는 "언제 화면을 다시 그릴 것인가"를 매우 신중하게 결정한다. + + +- 불필요한 재렌더링 +- 큰 컴포넌트 트리 +- 잦은 state 변경 + + +특히 **스크롤, 마우스 이동, 애니메이션** 같은 빈번한 업데이트는 React에게 부담이 된다. +
왜냐하면 본질적으로 **정적인 UI를 효율적으로 업데이트하는 데 최적화된 라이브러리** 이기 때문이다. +
+
+ +--- + +## 2. 3D 씬 렌더링 사이클 이해하기 + +- 3D 엔진(Three.js/WebGL)의 렌더링 방식은 완전히 다르다. +- 기본적으로 **매 프레임마다 장면을 다시 그린다.** + + + +
+ +이 과정은 "지속적인 루프" 이다. +
즉, React처럼 이벤트 기반이 아니라 **항상 돌아가는 애니메이션 루프 구조**다. + + +- draw call이 많을 때 +- geometry가 복잡할 때 +- material/shader 변경이 잦을 때 +- 텍스처 메모리 사용량이 클 때 +- 투명 오브젝트로 인한 오버드로우 +- CPU-side traversal 비용 증가 + + + + +### ✅ Scene Graph (씬 그래프) + +씬에 있는 모든 오브젝트들의 '가족관계도' 같은 트리 구조다 + +```txt title="게임 속 방 하나를 떠올려보면" +- 방 + ㄴ 책상 + ㄴ 컵 + ㄴ 노트북 + ㄴ 의자 + ㄴ 전동 +``` +### 왜 이 구조가 필요할까? +- 부모(책상)가 움직이면 자식(컵)도 자동으로 따라가게 만들기 위해 이 구조를 사용한다. +- 그래서 위치, 회전, 크기 계산에 사용되고 이는 3D 요소로서 필수적이다. +--- +### ✅ Draw Call (드로우 콜) + +CPU가 GPU에게 "이거 그려!"라고 요청하는 명령이다. + + +### ✅ Draw Call이 많아질수록 +- `CPU` -> `GPU` 지시가 늘어남 -> 준비 시간 늘어남 -> 성능 떨어짐 +- 그래서 **3D 최적화**에서 `draw call 줄이기 = 핵심 전략` +--- + +### ✅ CPU-side Traversal (CPU 사이드 트래버설) + +CPU가 씬에 있는 모든 물체를 하나씩 확인하는 과정 + + +### CPU도 매 프레임마다 하나씩 체크 -> 물체가 많아지면? +- 확인해야 할 대상 증가 ↑ -> CPU 부담 ↑ -> 프레임 드랍 발생 +--- +### ✅ OverDraw (오버드로우) + +같은 픽셀을 여러번 그리는 낭비 작업 + + +### 언제 발생할까? +- 투명 물체가 많을 때 +- 파티클 효과 많을 때 +- 겹쳐진 오브젝트 많을 때 +- GPU가 같은 픽셀을 여러번 계산 -> 성능 저하 원인 + + +--- +### 🎾 표로 한눈에 보기 +| 구분 | React | 3D 렌더링 | +| ----------------- | -------------------------------------- | ------------------------------- | +| **렌더링 트리거** | 상태(State)나 Props 변경 시 | 매 프레임마다 | +| **렌더링 주기** | 필요할 때만 다시 그림 (비동기적) | 1초에 60번 반복 (동기적 루프) | +| **관심 대상** | DOM / UI 트리 | 3D 씬 / 카메라 / 오브젝트 | +| **업데이트 방식** | Virtual DOM 비교 후 변경된 부분만 갱신 | 전체 씬을 매 프레임 다시 렌더링 | + +--- + +## 3. 문제 1 : 병목 -> 성능 이슈 +```tsx title="매 프레임 state 업데이트" +// 3D 루프는 초당 60번 실행, React도 초당 60번 재렌더 -> JS 메인 스레드 과부하 -> 결과: 프레임 드랍 +useFrame(() => { + setPosition(prev => prev + 1); +}); +``` + +```tsx title="React 렌더마다 Three.js 객체 재생성" +// 렌더마다 실행되면 -> GPU 리소스 재업로드 -> 메모리 사용 증가 -> draw call 준비 비용 증가 +const material = new THREE.MeshStandardMaterial(); +``` + +```txt title="씬 구조를 자주 변경하는 경우" +// React가 오브젝트를 mount/unmount 하면 +- Scene graph 재구성 +- CPU traversal 비용 증가 +- draw call 재정렬 +``` + + +- React 재렌더 +- 씬 재구성 +- CPU traversal 증가 +- draw call 증가 +- GPU 준비 비용 증가 + +위의 과정이 반복되면서 병목이 발생하게 된다. + + + +--- +## 4. 문제 2 : 불일치 -> UX 이슈 + +구체적인 예시 : +사용자가 방 테마를 변경하면 그에 따라 3D 모델 경로도 변경되어야 한다. + + + +- 핵심: React는 "상태 변경 → 재렌더"로 끝나지만, +- Three.js는 그 이후에 "리소스 교체 + GPU 반영"까지 진행되어야 실제 화면이 바뀐다. + +아래는 **사용자가 버튼을 클릭한 순간**부터 **테마(=modelPath)가 교체되어 화면에 반영될 때까지**를 +React 관점 vs Three.js 관점으로 **시간축에 따라** 정리한 플로우차트다. + + + +### 1) React vs Three.js : 불일치 구간이 존재 + +
+ +위 플로우차트를 보면 React와 Three.js가 **서로 다른 속도로 움직인다**는 걸 알 수 있다. +문제는 이 속도 차이가 단순한 이론이 아니라, **실제 사용자 경험과 성능에 직접적인 영향**을 준다는 점이다. + +### 아래는 실제로 자주 발생하는 대표적인 불일치 포인트들이다. + + +- React는 상태 변경 이후 매우 빠르게 UI를 업데이트한다. `setTheme → 재렌더 → 커밋 → UI 반영` +- 하지만 Three.js는 `기존 모델 제거 → 새 모델 비동기 로딩 → scene 반영`이라는 과정을 거쳐야 하기 때문에 시간이 더 걸린다. + + +### 🙋🏻 이때 사용자 입장에서는 이런 상황이 벌어진다 +- 버튼은 이미 `Desert`로 활성화됨 +- 텍스트/UI도 `Desert` 기준 +- 하지만 3D 씬은 여전히 `Forest`이거나 잠깐 아무 모델도 없는 빈 화면이 보임 + + + +즉, **UI와 3D가 서로 다른 상태를 보여주는 구간**이 발생한다. +
이 구간이 길어질수록 `깜박임`, `몰입감 저하`, `신뢰도 하락` 으로 이어진다. +
+
+ + +- 모델을 교체할 때 단순히 `scene.remove()`만 하면 충분하지 않다. + + +- `geometry` / `material` / `texture` / `buffer` + + + +이것들을 `dispose()` 하지 않으면 GPU 메모리는 계속 누적된다. + + +### 결과적으로 +- 테마 변경이 반복될수록 -> draw call 증가 -> GPU 메모리 사용량 증가 -> 프레임 드랍 발생 +- 이 문제는 코드 레벨에서는 보이지 않지만, 실제 UX에서는 명확하게 드러난다. + + + + +- 모델 로딩은 비동기다. 그래서 사용자가 빠르게 여러 번 클릭하면 이런 일이 벌어진다. + +- `클릭1` → Desert 로딩 시작 +- `클릭2` → Forest 로딩 시작 + + +- 만약 Desert 로딩이 더 늦게 끝나면 : Forest 상태인데 Desert 모델이 화면에 등장 +- 즉, **마지막 상태와 다른 결과가 화면에 나타나는 문제**가 생긴다. + + +### 결국 핵심은 +- React는 **상태 기준**으로 움직이고 +- Three.js는 **프레임 기준**으로 움직인다. + +➡️ 이 둘을 그냥 연결하면 **UI는 이미 미래로 갔는데, 3D는 아직 과거에 머무르는 상황이 발생**한다. + + +- 업데이트 타이밍 제어 +- 리소스 생명주기 관리 +- 비동기 로딩 동기화 + +위의 3가지가 필요하며, 이러한 지점에서 **R3F와 같은 동기화 레이어가 왜 필요한지** 드러난다. + + + + +--- +## 5. 그래서 필요한 것 — 동기화 레이어 + +### 이 문제를 해결하려면 +- React가 3D 프레임 루프에 끌려가지 않도록 하고 +- 3D가 React 상태 변경에 과하게 반응하지 않도록 해야 한다. + + +즉, **누가 언제 업데이트할지 조율해주는 레이어**가 필요하다. + +--- + +## 6. R3F는 무엇을 해결해줄까? + +R3F는 React와 Three.js 사이의 "브릿지" 역할을 한다. + + + + +**[ React는 “구조만” 관리 ]** +- 어떤 객체가 있어야 하는지만 정의 +- 매 프레임 계산은 하지 않음 + + + +**[ Three.js는 프레임 루프 유지 ]** +- R3F가 내부적으로 루프 관리 +- React 재렌더와 분리 + + + +**[ 객체 단위 diff 처리 ]** +- 전체 씬 재생성이 아니라 +- 필요한 부분만 업데이트 + + +### ✨ 코드 구조는 이렇게 단순해진다 (하지만 “진짜” 중요한 건 내부에서 일어나는 일) + +```tsx +function RoomModel({ modelPath }) { + const { scene } = useGLTF(modelPath); + return ; +} +``` + +R3F의 가치가 드러나는 지점은 “코드가 짧다”가 아니라, +**React 렌더링과 Three 렌더링 사이에서 어떤 레벨로 동기화가 일어나는지**다. + +
+ +### 🚨 R3F가 “상태 변화”를 처리하는 레벨 : DOM이 아니라 “Three 객체 그래프” + +React는 원래 DOM을 대상으로 diff를 계산한다. +R3F는 같은 방식으로, **Three.js 객체의 트리**를 대상으로 diff를 계산한다. + + +즉, R3F에서 `` 같은 JSX는 -> “Three 객체를 생성/수정하는 선언”으로 바뀐다. + +### 그래서 중요한 차이 +- React DOM: `div`/`span` 같은 DOM 노드 patch +- R3F: `THREE.Mesh`, `THREE.Material`, `THREE.Geometry` 같은 **Three 객체 patch** + + +이 덕분에 “전체 씬을 갈아끼우는” 게 아니라 **바뀐 속성만 객체에 반영**한다. + + +
+ +### 🚨 “렌더링 주도권”을 분리한다: React 렌더 ≠ Three 프레임 + +직접 구현하면 이런 실수를 하기 쉽다. + +- 애니메이션 값이 바뀔 때마다 `setState`를 쏴서 React가 60fps로 재렌더 +- 씬을 구성하는 객체를 매 렌더마다 재생성 + + + +### React가 하는 일 +- “무엇이 씬에 있어야 하는지”를 선언(구성) +- 상태가 바뀌면 “필요한 변경”만 일으킴 + +### Three가 하는 일 +- “어떻게 그릴지”를 프레임 루프로 수행(실제 렌더) + + +
+ +### 🚨 R3F의 diff가 강력한 이유 : “객체 재생성”이 아니라 “속성 패치” + +예를 들어 `position`만 바뀌는 상황을 보자. + + +```tsx title="잘못된 패턴(자주 보이는 병목" +// position이 바뀔 때마다 mesh를 새로 만들거나 +// material을 매 렌더마다 새로 생성 +const material = new THREE.MeshStandardMaterial({ metalness: 0.2 }); +// ❌ 렌더마다 새 material 생성 -> GPU 상태 변경/업로드 비용 증가 +``` + +- mesh는 유지, 바뀌는 값만 패치 + +```tsx title="✅ 동일 객체 유지 + 속성만 패치" + + + +``` + +**핵심은 “GPU에 올린 자원을 가능한 유지”** 하는 방향으로 흐름이 잡힌다는 점이다. + + + +
+ +### 🚨 로딩 캐싱을 “상태 동기화” 관점으로 보기 : 깜빡임/레이스 컨디션 완화 + +앞에서 말한 불일치(깜빡임)과 레이스 컨디션은 대부분 “비동기 로딩”이 원인이었다. + + + +### 1) 동일 경로 재요청 시 재사용(캐싱) +- 같은 모델을 다시 선택했을 때 “다시 로딩”하지 않고 재사용 + +### 2) Suspense와 결합하면 “빈 화면 구간”을 통제 가능 +- 로딩 중일 때 fallback UI를 보여주거나 +- 기존 모델을 유지한 채 로딩 완료 후 교체하는 UX 설계가 쉬워진다 + + +즉, 로딩 자체를 없애는 게 아니라, **로딩 구간을 “사용자가 납득 가능한 형태로” 연출할 수 있게 된다.** + + +
+ +### 🚨 dispose는 “자동”이라기보다 “생명주기”를 React 모델로 가져온 것 + + +- `scene.remove`만 하고 `dispose` 누락 +- 텍스처/머티리얼이 GPU에 남음 +- 반복 교체하면 프레임이 서서히 죽는다 + +R3F는 이걸 React의 생명주기(unmount)와 연결해서 다룰 수 있게 만든다. + + +- 컴포넌트가 `unmount`되면 +- 관련 객체/리소스를 정리(dispose)할 수 있는 구조를 제공한다. + +
+ +### 여기서 중요한 포인트는 +- “정리가 된다”가 아니라 +- “정리될 타이밍을 예측 가능하게 만든다” + + +즉, **리소스 생명주기를 코드 구조로 강제**한다. + + +
From a04caf39a0fef6d40c442b609380a21f04ff99b6 Mon Sep 17 00:00:00 2001 From: choiboa Date: Sat, 31 Jan 2026 04:26:00 +0900 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=93=9D=20Chore=20:=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EB=B8=94=EB=9F=AD=20=EC=83=89=20&=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EA=B5=90=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/posts/INSIGHT/r3f-renderingcycle.mdx | 4 +++- src/styles/mdx.css | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/content/posts/INSIGHT/r3f-renderingcycle.mdx b/content/posts/INSIGHT/r3f-renderingcycle.mdx index fa9ee87..790d389 100644 --- a/content/posts/INSIGHT/r3f-renderingcycle.mdx +++ b/content/posts/INSIGHT/r3f-renderingcycle.mdx @@ -44,6 +44,7 @@ draft: false alt: "리액트 렌더링 사이클 흐름 이미지", }, ]} + caption="React 렌더링 사이클" maxWidth="80%" /> @@ -82,6 +83,7 @@ draft: false alt: "3D 렌더링 사이클 흐름 이미지", }, ]} + caption="3D 렌더링 사이클" maxWidth="80%" /> @@ -208,7 +210,7 @@ React 관점 vs Three.js 관점으로 **시간축에 따라** 정리한 플로
Date: Sat, 31 Jan 2026 04:27:49 +0900 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=93=9D=20Chore=20:=20=EB=88=84?= =?UTF-8?q?=EB=9D=BD=EB=90=9C=20=EC=84=A4=EB=AA=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/posts/JOURNAL/2025-recap.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/posts/JOURNAL/2025-recap.mdx b/content/posts/JOURNAL/2025-recap.mdx index 9696a05..645a438 100644 --- a/content/posts/JOURNAL/2025-recap.mdx +++ b/content/posts/JOURNAL/2025-recap.mdx @@ -5,7 +5,7 @@ date: "2025-12-31" category: "Journal" series: "annual-journal" tags: ["", ""] -summary: "이 글에서 다루는 핵심 내용을 한두 줄로 요약합니다." +summary: "2025년 한 해를 돌아보며 남기는 연간 리캡" thumbnail: "" draft: false --- From 60c737191d32fc341a4f7ff5d0df5d0a23d2e33d Mon Sep 17 00:00:00 2001 From: choiboa Date: Sat, 31 Jan 2026 14:24:26 +0900 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=93=9D=20Chore=20:=20=EC=9D=B8?= =?UTF-8?q?=EB=9D=BC=EC=9D=B8=EC=BD=94=EB=93=9C=20=EC=83=89=EC=83=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=82=B4=EC=9A=A9=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- content/posts/INSIGHT/r3f-renderingcycle.mdx | 12 ++++++------ src/styles/mdx.css | 14 +++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/content/posts/INSIGHT/r3f-renderingcycle.mdx b/content/posts/INSIGHT/r3f-renderingcycle.mdx index 790d389..0d1d242 100644 --- a/content/posts/INSIGHT/r3f-renderingcycle.mdx +++ b/content/posts/INSIGHT/r3f-renderingcycle.mdx @@ -228,9 +228,9 @@ React 관점 vs Three.js 관점으로 **시간축에 따라** 정리한 플로 ### 🙋🏻 이때 사용자 입장에서는 이런 상황이 벌어진다 -- 버튼은 이미 `Desert`로 활성화됨 -- 텍스트/UI도 `Desert` 기준 -- 하지만 3D 씬은 여전히 `Forest`이거나 잠깐 아무 모델도 없는 빈 화면이 보임 +- 버튼은 이미 `테마2`로 활성화됨 +- 텍스트/UI도 `테마2` 기준 +- 하지만 3D 씬은 여전히 `테마1`이거나 잠깐 아무 모델도 없는 빈 화면이 보임 @@ -259,11 +259,11 @@ React 관점 vs Three.js 관점으로 **시간축에 따라** 정리한 플로 - 모델 로딩은 비동기다. 그래서 사용자가 빠르게 여러 번 클릭하면 이런 일이 벌어진다. -- `클릭1` → Desert 로딩 시작 -- `클릭2` → Forest 로딩 시작 +- `클릭1` → 테마2 로딩 시작 +- `클릭2` → 테마1 로딩 시작 -- 만약 Desert 로딩이 더 늦게 끝나면 : Forest 상태인데 Desert 모델이 화면에 등장 +- 만약 테마2 로딩이 더 늦게 끝나면 : 테마1 상태인데 테마2 모델이 화면에 등장 - 즉, **마지막 상태와 다른 결과가 화면에 나타나는 문제**가 생긴다. diff --git a/src/styles/mdx.css b/src/styles/mdx.css index 5d53886..8f7eaa9 100644 --- a/src/styles/mdx.css +++ b/src/styles/mdx.css @@ -83,17 +83,21 @@ figure[data-rehype-pretty-code-figure] pre[data-language="txt"] code { Inline code -------------------------- */ .prose code[data-language]:not(pre code[data-language]) { - font-size: 12px; - font-weight: 400; - padding: 0.2rem 0.4rem; + font-weight: 300; + padding: 0.3rem 0.4rem; border-radius: 0.2rem; - background: rgba(74, 74, 74, 0.476); - color: white; + background: #e9ecef; + color: #212529; white-space: nowrap; } +.dark .prose code[data-language]:not(pre code[data-language]) { + background: rgba(112, 112, 112, 0.476); + color: white; +} + .prose code[data-language]:not(pre code[data-language]) > span[data-line] { display: inline; } From 6f2106cda63e93fc87f0b9082c6c070c7e674461 Mon Sep 17 00:00:00 2001 From: choiboa Date: Sun, 1 Feb 2026 19:53:55 +0900 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=93=9D=20Chore=20:=20SEO=20=EB=A9=94?= =?UTF-8?q?=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 87ec62a..7c8f883 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -7,10 +7,10 @@ import "./globals.css"; export const metadata: Metadata = { metadataBase: new URL("https://b0o0a.com"), title: { - default: "B-log", + default: "B-log | 프론트엔드 개발 기술 블로그", template: "%s | B-log", }, - description: "FE 개발자 최보아의 프로젝트, 기술 인사이트, 개발 기록 블로그", + description: "프론트엔드 개발자 최보아의 기술 블로그. Next.js · React · 테스트 · 성능 최적화 · SEO · Three.js", icons: { icon: "/favicon.svg", }, @@ -21,9 +21,28 @@ export const metadata: Metadata = { openGraph: { type: "website", siteName: "B-log", + url: "https://b0o0a.com", + title: "B-log | 프론트엔드 개발 기술 블로그", + description: "프론트엔드 개발자 최보아의 기술 블로그, 포트폴리오", + images: [ + { url: "/post-fallback.png", width: 1200, height: 630, alt: "B-log" }, + ], + locale: "ko_KR", }, + alternates: { - canonical: "/", + canonical: "https://b0o0a.com", + }, + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-image-preview": "large", + "max-snippet": -1, + "max-video-preview": -1, + }, }, };