- );
-}
-
-// After: 12 lines
-export default function FeedPage() {
- return (
-
-
-
- );
-}
-```
-
-**결과:**
-
-- 중복 코드 60% 감소
-- 일관성 자동 보장
-- 레이아웃 변경 1곳에서 관리
-
----
-
-## 6. 성능 지표
-
-### Lighthouse 점수 (Desktop)
-
-- **Performance:** 98점
-- **Accessibility:** 95점
-- **Best Practices:** 100점
-- **SEO:** 100점
-
-### 핵심 Web Vitals
-
-- **LCP:** 1.2s (목표: < 2.5s)
-- **FID:** 45ms (목표: < 100ms)
-- **CLS:** 0.05 (목표: < 0.1)
-
-### 번들 사이즈
-
-- **First Load JS:** 87.2 kB
-- **Page Load (Home):** 92.5 kB
-- **Page Load (Feed Detail):** 105.3 kB
-
----
-
-## 7. AI와 함께한 개발 여정
-
-### 7.1 개발 환경
-
-이 블로그를 만드는 데 **약 일주일**이 걸렸어요.
-하지만 혼자는 아니었다.
-
-**사용한 도구:**
-
-- **Antigravity IDE** - AI 통합 개발 환경
-- **Claude Code Pro** - 구독하여 매일 토큰 한도까지 사용
-- **Gemini Pro** - 구독하여 매일 토큰 한도까지 사용
-
-매일 **토큰을 단 하나도 남기지 않고** 사용했다.
-그만큼 AI와 밀도 높은 페어 프로그래밍을 했다는 뜻이다.
-
-### 7.2 AI 페어 프로그래밍 방식
-
-**Claude Code의 역할:**
-
-- 아키텍처 설계 논의
-- 컴포넌트 구현
-- 리팩토링 제안 및 실행
-- 타입 정의 및 검증 로직 작성
-- 최적화 포인트 발견
-
-**Gemini Pro의 역할:**
-
-- 디자인 시스템 구조 논의
-- CSS 변수 네이밍 컨벤션
-- 성능 최적화 아이디어
-- MDX 처리 파이프라인 설계
-- 에러 해결 및 디버깅
-
-**내 역할:**
-
-- 요구사항 정의 및 우선순위 결정
-- AI 제안 검토 및 최종 판단
-- 코드 리뷰 및 품질 관리
-- 디자인 감각과 UX 의사결정
-
-### 7.3 AI 활용 패턴
-
-**1. 설계 단계**
-
-```
-나: "MDX 기반 블로그 시스템을 만들고 싶어. SSG로 구현하려고 하는데 구조를 어떻게 잡을까?"
-AI: [파일 구조 제안, 데이터 fetching 전략, 타입 정의 예시]
-나: "좋아, 근데 meta.json을 분리한 이유가 뭐야?"
-AI: [YAML frontmatter 이슈 설명, Zod 검증 이점]
-```
-
-**2. 구현 단계**
-
-```
-나: "TOC를 자동 생성하고 싶은데 어떻게 구현할까?"
-AI: [cheerio 활용 방안, IntersectionObserver 예시 코드]
-나: "이 코드 동작은 하는데 성능이 걱정되는데?"
-AI: [최적화 포인트 제안, rootMargin 조정, debounce 추가]
-```
-
-**3. 리팩토링 단계**
-
-```
-나: "이 페이지들 레이아웃 코드가 중복인데 어떻게 정리할까?"
-AI: [PageLayout 컴포넌트 추상화 제안]
-나: "근데 너무 일찍 추상화하는 거 아닌가?"
-AI: [중복이 3번 이상일 때 추상화 권장, 현재 2번이므로 보류 가능]
-→ 결국 2개 페이지지만 패턴이 명확해서 추상화 진행
-```
-
-### 7.4 AI 없이는 불가능했을 것들
-
-**1. Phase별 리팩토링**
-
-- 혼자였다면 "나중에 해야지"로 미뤘을 최적화
-- AI가 구체적인 Before/After 코드를 제시해서 바로 실행 가능
-
-**2. 타입 안전성**
-
-- Zod 스키마 작성, 에러 처리 로직
-- 혼자였다면 `any` 남발했을 부분을 타입 안전하게
-
-**3. 성능 측정 및 개선**
-
-- Lighthouse 점수 분석
-- Bundle Analyzer 해석
-- 병목 지점 정확히 찾아내기
-
-**4. 문서화**
-
-- 이 포스트 자체도 커밋 로그 분석하며 AI와 함께 작성
-- 설계 의도, Trade-off 설명을 논리적으로 구조화
-
-### 7.5 AI 시대의 개발자 역할
-
-AI가 코드를 작성해주지만, **개발자의 역할은 더 중요해졌다.**
-
-**여전히 내가 해야 할 것:**
-
-- **문제 정의**: "뭘 만들 것인가"
-- **우선순위 판단**: "지금 할 것인가, 나중에 할 것인가"
-- **품질 관리**: "이 코드가 정말 좋은가"
-- **디자인 감각**: "이게 사용자에게 좋은 경험인가"
-- **아키텍처 판단**: "이 구조가 확장 가능한가"
-
-**AI의 강점:**
-
-- 구현 속도 (10배 이상)
-- 일관성 있는 코드 패턴
-- 엣지 케이스 고려
-- 리팩토링 제안
-- 문서화 자동화
-
-**결론:**
-AI는 **페어 프로그래밍 파트너**다.
-내가 "무엇을, 왜"를 정의하면, AI가 "어떻게"를 빠르게 구현한다.
-그리고 내가 최종 품질을 검토하고 결정한다.
-
-일주일 동안 토큰을 전부 사용하며 만든 이 블로그는,
-**AI 없이 혼자 만들었다면 한 달은 걸렸을 것**이다.
-
----
-
-## 8. 배운 점
-
-### 8.1 설계 원칙
-
-"완벽한 디자인 시스템보다 일관성 있는 구현이 중요하다."
-
-초반에 완벽한 디자인 시스템을 만들려 했지만, 실제 구현 과정에서 일관성이 깨졌다.
-Phase별 리팩토링을 통해 일관성을 확보하는 것이 더 효과적이었다.
-
-"Server Component 기본, Client Component 최소화."
-
-모든 컴포넌트를 'use client'로 만들면 편하지만,
-Server Component를 최대한 활용하면 번들 사이즈와 성능에서 큰 이점을 얻을 수 있다.
-
-"추상화는 중복이 3번 이상 발생할 때."
-
-PageLayout 컴포넌트를 초반에 만들지 않고,
-2개 페이지에서 중복이 명확해진 시점에 추상화했다.
-조기 추상화는 오히려 유연성을 해칠 수 있다.
-
-### 8.2 기술 선택
-
-"최신 기술은 생산성을 위해, 안정성도 함께 고려."
-
-- Next.js 16: App Router의 안정성 확보
-- Tailwind v4: 빌드 속도 개선 체감
-- MDX: React 통합으로 확장성 극대화
-
-"번들 사이즈는 항상 모니터링."
-
-Three.js 같은 무거운 라이브러리는 코드 스플리팅 필수.
-`next/dynamic`과 `'use client'`를 조합하여 초기 로딩에 영향 없도록 관리.
-
-### 8.3 최적화
-
-"측정하지 않으면 최적화할 수 없다."
-
-Lighthouse, Bundle Analyzer를 통해 정량적 지표를 확보했다.
-추측이 아닌 데이터 기반 최적화다.
-
-"점진적 개선이 급진적 리팩토링보다 안전하다."
-
-Phase 1~5로 나눠 단계별로 개선했다.
-각 Phase마다 빌드 검증 → 커밋 → 다음 단계.
-롤백 가능한 지점을 명확히 했다.
-
----
-
-## 9. 다음 목표
-
-### 9.1 기능 추가
-
-- 검색 기능 (Algolia or local index)
-- RSS Feed 생성
-- 댓글 시스템 (giscus)
-- OG Image 자동 생성
-
-### 9.2 성능 최적화
-
-- Image 최적화 (WebP, AVIF)
-- Critical CSS inline
-- Font preloading 전략
-
-### 9.3 개발 경험
-
-- Storybook 도입
-- E2E 테스트 (Playwright)
-- 단위 테스트 커버리지 80%+
-
----
-
-## 마무리
-
-기술 블로그를 직접 만드는 일은 단순히 글 쓰는 공간을 만드는 것 이상이었다.
-
-- 어떤 기술을 선택할지 판단하고
-- 어떤 경험을 줄지 설계하고
-- 어떤 품질 기준으로 유지할지 계속 검증해야 했다
-
-이번 구축 이후 남은 기준은 세 가지다.
-
-1. 블로그도 제품처럼 목적과 정보 구조가 먼저 정해져야 한다.
-2. 성능 최적화는 구현이 끝난 뒤가 아니라 구조를 잡는 시점부터 같이 가야 한다.
-3. AI는 구현 속도를 올리지만, 품질 기준과 최종 판단은 사람이 끝까지 가져가야 한다.
-
-이 글이 기술 블로그를 만들려는 사람에게
-"어떤 스택을 썼는가"보다 "어떤 기준으로 만들었는가"를 보여주는 참고가 되길 바란다.
-
-**코드는 여기에:** [eunu.log GitHub](https://github.com/dev-wooyeon/eunu.log)
-
----
-
-## 참고 자료
-
-**영감을 받은 프로젝트:**
-
-- [zerolog.vercel.app](https://zerolog.vercel.app) - 디자인과 구조의 영감
-
-**기술 문서:**
-
-- [Next.js 16 Documentation](https://nextjs.org/docs)
-- [MDX Documentation](https://mdxjs.com/)
-- [Tailwind CSS v4](https://tailwindcss.com/)
-- [Three.js + React Three Fiber](https://docs.pmnd.rs/react-three-fiber)
+1. 화면을 추가하기 전에 변경 이유와 소유 모듈을 정한다.
+2. 새 글은 public이 아니라 private에서 시작하고, 메타데이터와 본문을 함께 검토한다.
+3. 콘텐츠 파이프라인 변경은 설정 정리가 아니라 마이그레이션으로 취급한다.
+4. 읽기 화면의 새 시각 요소는 본문보다 먼저 보이는지 검토한다.
+5. 성능과 생산성의 결론은 수치가 아니라 재현 가능한 측정과 검증 명령이 있을 때만 쓴다.
+
+이 글은 현재 Ark의 구조와 운영 기준을 기록한 회고다. 공개 여부는 다시 한 번
+콘텐츠 리뷰를 거쳐 결정한다. 구조를 바꾸기 전에 무엇을 지켜야 하는지 확인할 때,
+이 기준을 다시 사용한다.
diff --git "a/posts/\353\270\224\353\241\234\352\267\270-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225\352\270\260/meta.json" "b/posts/\353\270\224\353\241\234\352\267\270-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225\352\270\260/meta.json"
index b8bfb463..16885588 100644
--- "a/posts/\353\270\224\353\241\234\352\267\270-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225\352\270\260/meta.json"
+++ "b/posts/\353\270\224\353\241\234\352\267\270-\354\213\234\354\212\244\355\205\234-\352\265\254\354\266\225\352\270\260/meta.json"
@@ -1,31 +1,25 @@
{
- "title": "Next.js 16과 Three.js로 블로그를 구축하고 최적화한 기록",
+ "title": "Next.js와 MDX로 다시 설계한 기술 블로그",
"slug": "blog-system-building",
- "description": "MDX 기반 기술 블로그를 만들며 정리한 설계 원칙, 시스템 구조, 성능 최적화 기준을 소개한다.",
+ "description": "글과 UI를 함께 운영하기 위한 Ark의 현재 구조, MDX 계약, 공개 정책, 검증 기준을 기록한다.",
"date": "2026-01-28",
"category": "Tech",
"contentType": "retrospective",
- "tags": [
- "Next.js",
- "React",
- "MDX",
- "Three.js",
- "Design System"
- ],
+ "tags": ["Next.js", "React", "MDX", "Design System"],
"featured": false,
"visibility": "private",
"qualityReview": {
- "philosophy": 3,
+ "philosophy": 4,
"design": 4,
"implementation": 4,
- "brandFit": 2.5,
- "clarity": 3.5,
- "structure": 3.5,
+ "brandFit": 4,
+ "clarity": 4,
+ "structure": 4,
"evidence": 3.5,
"usefulness": 3.5,
- "originality": 3.5,
- "polish": 2.5,
- "reviewedAt": "2026-07-13",
- "notes": "현재 저장소 구조와 성능 지표에 맞게 갱신하기 전까지 비공개"
+ "originality": 4,
+ "polish": 4,
+ "reviewedAt": "2026-07-20",
+ "notes": "현재 Ark 구조와 ADR에 근거해 전면 재작성했다. 공개 승격은 별도 콘텐츠 리뷰에서 결정한다."
}
}
diff --git "a/posts/\354\225\214\352\263\240\353\246\254\354\246\230-\354\213\234\352\260\201\355\231\224/index.mdx" "b/posts/\354\225\214\352\263\240\353\246\254\354\246\230-\354\213\234\352\260\201\355\231\224/index.mdx"
index 989e4b91..f8445177 100644
--- "a/posts/\354\225\214\352\263\240\353\246\254\354\246\230-\354\213\234\352\260\201\355\231\224/index.mdx"
+++ "b/posts/\354\225\214\352\263\240\353\246\254\354\246\230-\354\213\234\352\260\201\355\231\224/index.mdx"
@@ -1,17 +1,8 @@
-import {
- BinarySearchVisualization,
- DPVisualization,
- GraphTraversalVisualization,
- SlidingWindowVisualization,
- SortingVisualization,
- TwoPointerVisualization,
-} from '@/blog/ui/mdx/visualization-components';
-
## 코딩테스트 알고리즘 완전정복
코딩테스트에서 자주 출제되는 **핵심 알고리즘과 자료구조**를 한 곳에 모았어요.
각 알고리즘의 **핵심 개념**과 **언제 사용하는지**를 먼저 파악하면, 문제를 보는 순간 어떤 접근을 꺼내야 할지 더 빨리 판단할 수 있어요.
-그래프 탐색부터 정렬, DP, 그리디, 투 포인터까지 시각화와 함께 훑어보며 기본 선택 기준을 한 번에 잡아보면 좋아요.
+그래프 탐색부터 정렬, DP, 그리디, 투 포인터까지 핵심 선택 기준을 한 번에 잡아보면 좋아요.
---
@@ -54,11 +45,9 @@ import {
---
-### 🎮 DFS vs BFS 시각화
-
-아래 시각화로 두 알고리즘의 차이를 직접 확인할 수 있어요.
+### DFS와 BFS 비교
-
+두 알고리즘의 탐색 순서를 비교해 보면, 어떤 문제에서 각각이 적합한지 더 쉽게 판단할 수 있어요.
> **핵심 차이:** DFS는 **깊이 우선**(세로로), BFS는 **너비 우선**(가로로) 탐색해요.
@@ -122,11 +111,9 @@ import {
- 메모리가 제한적이면서 O(n log n)을 보장해야 할 때
-### 🎮 정렬 알고리즘 시각화
+### 정렬 알고리즘 비교
-아래 시각화로 세 가지 정렬 알고리즘의 동작 방식을 직접 확인할 수 있어요.
-
-
+세 가지 정렬 알고리즘은 모두 O(n log n) 평균 또는 보장 성능을 제공하지만, 메모리와 안정성의 조건이 달라요.
> **핵심 차이:** 퀵 정렬은 피벗 기준 분할, 병합 정렬은 분할 후 병합, 힙 정렬은 힙 구조 활용
@@ -164,11 +151,9 @@ while left <= right:
# 범위 조정
```
-### 🎮 이진 탐색 시각화
-
-정렬된 배열에서 이진 탐색이 어떻게 동작하는지 확인할 수 있어요.
+### 이진 탐색 핵심
-
+정렬된 배열에서는 탐색 범위를 절반씩 줄이는 방식으로 빠르게 값을 찾을 수 있어요.
> **핵심:** 매번 탐색 범위를 절반으로 줄여 O(log n) 시간에 탐색해요.
@@ -230,11 +215,9 @@ while left <= right:
- 최장 공통 부분 수열 (LCS)
- 편집 거리 (Edit Distance)
-### 🎮 DP 시각화 - 피보나치 수열
+### DP 핵심 - 피보나치 수열
-동적 프로그래밍이 어떻게 중복 계산을 제거하는지 확인할 수 있어요.
-
-
+동적 프로그래밍은 작은 문제의 결과를 저장해 중복 계산을 제거해요.
> **핵심:** 작은 문제의 결과를 저장하고 재사용해 O(2ⁿ)에서 O(n)으로 최적화해요.
@@ -373,11 +356,9 @@ while right < n:
- ✅ 구간 합이 특정 조건을 만족하는 경우 찾기
- ✅ 연속된 부분 배열 문제
-### 🎮 투 포인터 시각화
-
-두 포인터가 어떻게 이동하며 목표값을 찾는지 확인할 수 있어요.
+### 투 포인터 핵심
-
+두 포인터는 양 끝에서 시작해 조건에 따라 이동하며 목표값을 찾아요.
> **핵심:** 정렬된 배열에서 양 끝의 포인터를 조건에 따라 이동해 O(n) 시간에 해결해요.
@@ -423,11 +404,9 @@ for right in range(n):
# 윈도우 축소 (left++)
```
-### 🎮 슬라이딩 윈도우 시각화
-
-고정 크기 윈도우가 어떻게 이동하며 최대값을 찾는지 확인할 수 있어요.
+### 슬라이딩 윈도우 핵심
-
+고정 크기 윈도우는 한 칸씩 이동하며 이전 계산 결과를 재사용해요.
> **핵심:** 윈도우를 한 칸씩 이동하며 이전 계산 결과를 재사용해 O(n) 시간에 해결해요.
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/connectors/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/connectors/index.mdx"
deleted file mode 100644
index 25299386..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/connectors/index.mdx"
+++ /dev/null
@@ -1,272 +0,0 @@
-# Flink Connector 실전
-
-## Connector가 왜 중요한가?
-
-Flink는 "계산 엔진"일 뿐이고,
-
-실제 업무에서는 **데이터를 어디서 가져오고(Kafka, Filesystem), 어디에 저장하느냐(ClickHouse, Iceberg, JDBC)**가 파이프라인의 본질을 결정함.
-
-즉,
-- Flink = 파이프의 중앙 처리부
-- Connector = 파이프의 입구와 출구
-
-Connector 이해가 바로 파이프라인 설계 능력임.
-
----
-
-## Connector 전반 개념 정리
-
-Connector는 크게 두 가지로 나뉨:
-
-### Source Connector
-- Kafka, Pulsar, Kinesis
-- FileSystem(Parquet, CSV), Iceberg
-- JDBC(읽기)
-
-### Sink Connector
-- ClickHouse, JDBC, Kafka, Iceberg
-- FileSystem(Parquet/CSV)
-
-중요한 건 Flink Table API/SQL에서는
-
-**DDL로 선언만 하면 바로 테이블처럼 다룰 수 있다는 점.**
-
-예)
-```sql
-CREATE TABLE orders (...) WITH ('connector' = 'kafka');
-```
-
-이 한 줄로 "Kafka → Flink → SQL로 처리"가 됨.
-
----
-
-## Kafka Connector
-
-(가장 많이 쓰는 정석 Source/Sink)
-
-Kafka는 Flink에서 사실상 **표준 Source**임.
-
-### Kafka Source 특징
-- 파티션 단위 병렬 처리 → Flink parallelism 자동 확장
-- event-time 기반 처리에 적합 (timestamp & watermark)
-- exactly-once 보장 가능 (source offset + state 체크포인트)
-
-### Kafka Sink 특징
-- 모니터링 스트림, DLQ(dead-letter), alert pipeline 등에 자주 사용
-- 트랜잭션 기반 exactly-once 지원
-- key, partition, timestamp 메타데이터도 커스텀 가능
-
-**예제 DDL**
-```sql
-CREATE TABLE orders (
- order_id STRING,
- user_id STRING,
- ts TIMESTAMP(3),
- WATERMARK FOR ts AS ts - INTERVAL '5' SECOND
-) WITH (
- 'connector' = 'kafka',
- 'topic' = 'orders',
- 'properties.bootstrap.servers' = 'kafka:9092',
- 'format' = 'json'
-);
-```
-
-Kafka는 Flink의 "입구"로 가장 적합한 구조임.
-
----
-
-## ClickHouse Connector
-
-(실시간 대시보드/OLAP 지표용)
-
-ClickHouse는 Flink Sink로 가장 자주 쓰이며 목적은 단 하나:
-
-**"빠르게 조회 가능한 실시간 집계 테이블 만들기"**
-
-**ClickHouse가 잘하는 것:**
-- 대용량 집계, 그룹바이 속도
-- 실시간 대시보드/서비스 통계
-
-**ClickHouse Sink 패턴**
-- 보통 **집계된 형태(윈도우 결과)**를 넣음
-- row-level 데이터를 넣는 구조는 거의 없음
-- INSERT BATCH size, retry 정책이 중요
-
-**예제 DDL**
-```sql
-CREATE TABLE ch_sales (
- window_start TIMESTAMP(3),
- window_end TIMESTAMP(3),
- category STRING,
- total_amount DOUBLE
-) WITH (
- 'connector' = 'jdbc',
- 'url' = 'jdbc:clickhouse://ch-host:8123/default',
- 'table-name' = 'realtime_sales'
-);
-```
-
-요약:
-대표님이 만드는 **5분/10분 매출 집계**, **유저 행동 통계** 같은 건 ClickHouse가 최적임.
-
----
-
-## Iceberg Connector
-
-(Data Lake / Analytics / 장기 보관 / 스냅샷 관리)
-
-Iceberg는 "디스크 위의 거대한 테이블"을 관리하는 포맷.
-
-데이터 레이크의 사실상 표준이 되어가는 중.
-
-**Iceberg가 필요한 이유:**
-- 장기 보관 (수개월~수년)
-- 스키마 진화 지원
-- 대량 데이터 효율적 분할(파티션)
-- Time-Travel(특정 시점 스냅샷 조회)
-- Flink/Spark/Trino 등 여러 엔진에서 동시에 조회 가능
-
-**Flink + Iceberg 조합은 아래 목적에 최적화됨:**
-- row-level 원본 로그 저장
-- Fact 테이블 적재
-- batch/stream 통합 ETL
-- ML feature store용 원본 데이터
-
-**예제 DDL**
-```sql
-CREATE TABLE iceberg_orders (
- order_id STRING,
- user_id STRING,
- amount DOUBLE,
- ts TIMESTAMP(3),
- WATERMARK FOR ts AS ts - INTERVAL '10' SECOND
-) WITH (
- 'connector' = 'iceberg',
- 'catalog-name' = 'prod_catalog',
- 'catalog-type' = 'hive',
- 'warehouse' = 's3://warehouse/'
-);
-```
-
-요약:
-실시간 ClickHouse는 "지금 보는 지표",
-Iceberg는 "나중에 분석할 원본 저장소".
-
----
-
-## JDBC Connector
-
-(레거시 DB/차원 테이블 lookup/단순 적재)
-
-Flink의 JDBC Connector는 안정성은 좋지만 **대량 쓰기에는 부적합**임.
-
-대량 쓰기 환경에서는 ClickHouse·Iceberg·Kafka가 더 적합함.
-
-**JDBC는 아래 상황에 딱 맞음:**
-- Dim table 조회
-- 적은 row에 incremental load
-- 운영 DB에 소규모 로그 삽입
-- Lookup join으로 enrichment
-
-**예제 DDL**
-```sql
-CREATE TABLE dim_user (
- user_id STRING,
- grade STRING
-) WITH (
- 'connector' = 'jdbc',
- 'url' = 'jdbc:mysql://mysql:3306/db',
- 'table-name' = 'user_dim',
- 'username' = 'root',
- 'password' = 'root'
-);
-```
-
-**주의사항:**
-- 병렬 Insert는 DB 부하 커짐
-- Primary Key 기반 upsert만 제대로 활용
-- 트랜잭션 비용 큼
-→ 운영 DB는 과부하 위험 있으니 대부분은 "reference lookup"용으로만 사용함.
-
----
-
-## 실전: 왜 하나의 Kafka 주문 이벤트를 여러 목적지로 나누는가?
-
-아래는 실제 회사들이 가장 많이 쓰는 패턴.
-
-### ClickHouse: "지금 바로 보는 지표"
-- 1분/5분/10분 집계
-- 대시보드
-- 내부 운영툴
-
-### Iceberg: "나중에 또 분석할 raw 데이터"
-- row-level 주문 로그
-- batch 분석
-- ML 학습 데이터
-- Data Lake 저장
-
-### 모니터링 Kafka: "실시간 이상탐지/알림/다른 팀 소비"
-- 고액 주문 alert
-- 결제 실패 이벤트
-- 위험 패턴 감지
-
-### 이걸 Flink에서 어떻게 구현?
-
-StatementSet으로 한 번에 처리함.
-
-```java
-StatementSet stmt = tableEnv.createStatementSet();
-
-stmt.addInsertSql("INSERT INTO ch_sales SELECT ...");
-stmt.addInsertSql("INSERT INTO iceberg_orders SELECT ...");
-stmt.addInsertSql("INSERT INTO monitor_topic SELECT ...");
-
-stmt.execute();
-```
-
-하나의 ordering event가 이렇게 분기되는 것:
-
-```
-Kafka orders
- ├─ 집계 → ClickHouse
- ├─ 원본 row → Iceberg
- └─ 고액/이상 이벤트 → monitoring Kafka
-```
-
-이게 바로 "하나의 소스 → 여러 목적지" 패턴임.
-
----
-
-## 어떤 Connector를 언제 선택하면 좋은가?
-
-간단히 정리하면 아래 기준이면 대부분 해결됨.
-
-**실시간 집계/지표**
-→ ClickHouse
-
-**장기 보관 / 원본 저장 / BI·ML 분석**
-→ Iceberg
-
-**실시간 알림 / 이상탐지 / 확장성 높은 이벤트 브로커 / 외부 팀에서 사용해야하는 데이터**
-→ Kafka
-
-**운영 DB에 소규모 참조 / Dim table lookup**
-→ JDBC
-
----
-
-## 정리
-
-- Flink는 외부 시스템을 Table로 선언해 스트림/배치를 일관된 방식으로 처리
-- Kafka는 사실상 표준 Source이고, ClickHouse는 실시간 집계 Sink, Iceberg는 장기 보관용 Data Lake Sink, JDBC는 소규모 Lookup/적재에 사용함.
-- 실무에서는 하나의 Kafka 이벤트를 여러 목적지로 보내기 위해 StatementSet을 활용해 ClickHouse·Iceberg·모니터링 Kafka로 동시에 전송하는 구조가 일반적
-
----
-
-## 공식 문서 출처
-
-- [Flink Connectors 개요](https://nightlies.apache.org/flink/flink-docs-stable/docs/connectors/table/overview/)
-- [Kafka Connector](https://nightlies.apache.org/flink/flink-docs-stable/docs/connectors/table/kafka/)
-- [JDBC Connector](https://nightlies.apache.org/flink/flink-docs-stable/docs/connectors/table/jdbc/)
-- [Iceberg Connector](https://iceberg.apache.org/docs/latest/flink/)
-- [ClickHouse + JDBC 참고](https://nightlies.apache.org/flink/flink-docs-stable/docs/connectors/table/jdbc/)
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/connectors/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/connectors/meta.json"
deleted file mode 100644
index 73d4cc55..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/connectors/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "Flink Connector 실전",
- "slug": "flink-connectors",
- "description": "Kafka, ClickHouse, Iceberg, JDBC 등 주요 Connector의 활용법과 실전 아키텍처를 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "Kafka",
- "Connector"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 5
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 3.5,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/datastream-api/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/datastream-api/index.mdx"
deleted file mode 100644
index 2e90edaa..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/datastream-api/index.mdx"
+++ /dev/null
@@ -1,182 +0,0 @@
-# DataStream API 심화
-
-## 왜 DataStream API가 필요한가?
-
-Table API/SQL로 대부분의 파이프라인을 만들 수 있는데도,
-
-DataStream API가 존재하는 이유는 **"SQL로 표현하기 어려운 영역"**이 분명히 있기 때문임.
-
-**대표 케이스**
-1. 이벤트 단위 정밀 제어
-2. 타이머 기반 로직 (예: N초 동안 추가 이벤트 없으면 time-out)
-3. 비정형 데이터 처리
-4. 커스텀 상태(state) 기반 로직
-5. 여러 스트림을 복잡하게 결합하거나 패턴 감지
-
-DataStream API는 "스트림 처리의 실체를 직접 다루는 레벨"이라고 보면 됨.
-
----
-
-## DataStream API의 핵심 흐름
-
-DataStream은 크게 이렇게 흘러감:
-
-**Source → Transformation → Sink**
-
-Transformation 안에서 다음을 다룸:
-- keyBy
-- window
-- state
-- timers
-- process function
-- async IO
-- side output
-
-즉, Table API가 "추상화된 SQL"이라면,
-DataStream API는 "연산자(operator) 수준에서 직접 조립"하는 모델임.
-
----
-
-## DataStream을 이해하는 핵심 4요소
-
-### 1) KeyedStream — Stateful 처리의 절대 기반
-
-Flink의 모든 상태 기반 로직은 **keyBy**를 중심으로 돌아감.
-
-왜냐면:
-- 같은 key는 반드시 같은 subtask로 라우팅됨
-- 그래서 key 단위로 상태(state)를 안전하게 유지할 수 있음
-- window, timer, state, process function 모두 KeyedStream 위에서 동작
-
-즉, "key를 기준으로 파티션 → 동일 key를 가진 이벤트는 같은 CPU에서 처리"라는 모델임.
-
-이게 없으면 state는 유지 불가.
-
-### 2) Window — 시간 기반 집계의 핵심
-
-Table API의 TUMBLE/HOP 같은 window와 유사하지만, DataStream은 더 강함:
-
-- Trigger 제어 가능
-- Evictor로 데이터 제거 가능
-- Late data 정책을 훨씬 유연하게 설정 가능
-- 윈도우 별로 process 가능
-
-**예시**
-- "5초 내 들어온 이벤트 합계"
-- "유저별 30분 세션 종료 감지"
-- "지연 데이터 허용 범위 동적으로 조절"
-
-DataStream window는 정말 디테일하게 제어할 수 있는 도구라고 보면 됨.
-
-### 3) ProcessFunction — DataStream API의 진짜 힘
-
-"윈도우 없이도 이벤트 단위로 모든 걸 제어할 수 있게 해주는 함수"임.
-
-ProcessFunction 계열은 Flink의 하이엔드 기능이라고 보면 됨.
-
-**대표 기능**
-- 이전 이벤트 접근
-- 다음 이벤트를 기다릴지 말지 결정
-- 타이머 등록 (Event-time / Processing-time 모두 가능)
-- side output으로 조건별 흐름 분기
-- 상태(state) 직접 관리
-
-이걸 이해하면 "Flink로 뭘 할 수 있고, 어디까지 가능한가" 감이 확실히 잡힘.
-
-**대표적인 사용 예**
-- N초 동안 추가 이벤트 없으면 timeout → 주문 취소 처리
-- 두 개 이벤트 조합해 패턴 감지 (ex. 로그인 → 결제)
-- "지연 도착한 이벤트 but 아직 처리해야 하는 케이스" 세밀 제어
-
-### 4) State — Flink를 Flink답게 만드는 요소
-
-State는 "이전 이벤트의 정보를 메모리나 로컬 DB(RocksDB)에 저장해두는 것".
-
-DataStream API에서 제공하는 상태:
-- ValueState
-- ListState
-- MapState
-- ReducingState
-- AggregatingState
-
-**예시**
-- "유저별 최근 주문 3개 저장" → ListState
-- "장바구니 상태 업데이트" → MapState
-- "이벤트 누적 합계" → ReducingState
-
-이 상태는 checkpoint에 함께 저장되어 장애 복구되는 구조임.
-
----
-
-## 고급 기능 — 실무에서 가장 많이 쓰이는 것들
-
-### Async I/O — 외부 시스템 호출 시 무조건 고려하는 기능
-
-외부 DB/Redis/ML API 호출하면 병목 생김 → Flink 전반 backpressure 발생.
-
-**Solution = Async I/O**
-
-**특징**
-- 외부 요청을 비동기로 처리
-- backpressure 최소화
-- 병렬도 확장성 유지
-
-**예시**
-- user_id → Redis 프로필 조회
-- 상품 ID → 외부 정가 데이터 조회
-- ML inference API 호출
-
-### Side Output — 흐름을 조건으로 분기
-
-Table API에선 어려운 "조건 분기"를 DataStream API는 자연스럽게 지원함.
-
-**예시**
-- Late event만 별도 Kafka로
-- 잘못된 스키마 데이터만 "dead-letter-topic"으로
-- 특정 임계값 이상의 주문만 모니터링 토픽으로 송출
-
-### Backpressure — 성능과 안정성의 핵심 지표
-
-DataStream API를 운영할 때 가장 많이 겪는 문제.
-
-**원인**
-- Sink 데이터베이스 느림
-- 외부 API 느림
-- Window state 과도하게 큼
-- Serialization 무거움
-- 병렬도/리소스 부족
-
-**징후**
-- checkpoint duration 증가
-- watermark 지연
-- task queue 적체
-
-Flink 운영에서 "backpressure 이해"는 거의 필수임.
-
----
-
-## 현실적인 예시
-
-주문 이벤트 스트림 기준으로 예를 드리면 아래처럼 동작함.
-
-**주문이 들어올 때마다**
-- 유저별 최근 주문 n개 추적 (ListState)
-- 결제 실패 후 5초 내 재시도 없으면 알림 (KeyedProcessFunction + timer)
-- 특정 금액 이상 주문은 별도 Kafka로 전송 (side output)
-- 외부 DB에서 상품 카테고리 조회 (Async I/O)
-- 매 10분마다 집계 후 ClickHouse에 저장 (Window)
-
-이걸 전부 Table API로 하는 건 불가능함.
-
-그래서 DataStream API가 필수로 필요함.
-
----
-
-## 출처
-
-- [DataStream overview](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/overview/)
-- [State & Checkpoint](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/fault-tolerance/state/)
-- [Windows](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/operators/windows/)
-- [ProcessFunction](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/operators/process_function/)
-- [Async I/O](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/operators/asyncio/)
-- [Backpressure](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/monitoring/back_pressure/)
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/datastream-api/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/datastream-api/meta.json"
deleted file mode 100644
index 6300a21a..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/datastream-api/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "DataStream API 심화",
- "slug": "flink-datastream-api",
- "description": "SQL로 표현하기 어려운 복잡한 스트림 처리를 위한 DataStream API의 핵심 개념과 고급 기능을 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "DataStream",
- "Data Engineering"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 3
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 3.5,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/kubernetes/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/kubernetes/index.mdx"
deleted file mode 100644
index 29c4898e..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/kubernetes/index.mdx"
+++ /dev/null
@@ -1,239 +0,0 @@
-# Flink Kubernetes 운영
-
-Flink를 실무에서 안정적으로 운영하려면 결국 Kubernetes 위에서 돌아가는 구조를 이해해야 한다.
-
-Flink는 기본적으로 **분산 스트림 처리 엔진**이기 때문에, JobManager/TaskManager/Checkpoint Storage 등을 어떻게 배포·확장·복구하느냐가 운영 품질을 좌우한다.
-
-아래는 Kubernetes 기반 Flink 운영에서 꼭 알고 있어야 하는 핵심 개념과 실전 운영 기준을 정리한 내용이다.
-
----
-
-## Flink Kubernetes 운영 전략 전체 개요
-
-Flink Job은 크게 두 방식으로 Kubernetes에 배포된다.
-
-### 1. Flink Session Cluster
-- 하나의 JobManager + 여러 TaskManager
-- 여러 Job이 하나의 클러스터를 공유
-- 리소스 공유로 효율적이지만, 격리가 약하고 장애 파급 가능성 있음
-
-### 2. Flink Application Cluster (요즘 실무 표준)
-- 하나의 Job = 하나의 독립 클러스터
-- JobManager/TaskManager가 Job과 함께 생성/종료
-- 격리·안정성·배포 단순성이 좋아 대규모 서비스에서 주로 사용
-
-Session Cluster는 개발·테스트 환경,
-Application Cluster는 운영 환경에서 가장 적합하다.
-
----
-
-## Kubernetes + Flink의 기본 아키텍처
-
-```
-Kubernetes
- ├─ Deployment: Flink JobManager
- ├─ Deployment: Flink TaskManager
- ├─ Service: JobManager RPC / REST
- ├─ ConfigMap: flink-conf.yaml / log4j / libs
- └─ Persistent Volume: Checkpoint/Savepoint Storage
-```
-
-핵심은 JobManager의 안정성, TaskManager의 수평 확장, Checkpoint 저장소의 내구성 3가지다.
-
----
-
-## JobManager 운영 포인트
-
-### 단일 장애 지점을 막기 위한 고가용성(HA)
-
-Kubernetes에서는 기본적으로 아래 방식으로 고가용성을 구현한다.
-
-- JobManager Deployment → replica 1 유지
-- Zookeeper 또는 Kubernetes-native HA 사용
-- JobManager 실패 시 새로운 파드가 뜰 때 checkpoint에서 복구
-
-**주의할 점:**
-JobManager는 "상태 없는 컨트롤 플레인처럼 보이지만", 실제로는 checkpoint/metadata 관리 때문에 반드시 **외부 고가용성 스토리지**가 필요하다.
-
-**추천 구성**
-- HA Storage → S3 / GCS / HDFS
-- HA Metadata → Kubernetes HA (Flink v1.15+ 권장)
-
----
-
-## TaskManager 운영 포인트
-
-TaskManager는 전부 "소비자(worker)" 역할이다.
-
-**CPU/메모리 요청·제한 설정**
-
-TaskManager는 JVM + RocksDB + 네트워크 버퍼까지 고려해야 한다.
-
-**예시:**
-```yaml
-resources:
- requests:
- cpu: "2"
- memory: "6Gi"
- limits:
- cpu: "3"
- memory: "8Gi"
-```
-
-**스케일 조절**
-
-병렬도(parallelism)가 올라가면 TaskManager 개수도 자동 증가한다.
-
-- 병렬도 32
-- TM당 slot 4
-→ 최소 TaskManager 8개 필요
-
-**노드 장애 대비**
-
-Kubernetes는 TaskManager가 죽으면 자동으로 재생성하지만,
-State는 checkpoint에서 복구되므로 크게 문제되지 않는다.
-
----
-
-## Checkpoint 저장소 (가장 중요한 운영 요소)
-
-Kubernetes 운영에서 checkpoint 저장소는 **사실상 S3가 정답**에 가깝다.
-
-**이유:**
-- Pod 재생성에도 state 유지 가능
-- HA 환경에서 공유 스토리지 필수
-- 병렬 checkpoint에 적합
-- Savepoint 보관에도 최적
-
-**폴더 예시:**
-```
-s3://my-bucket/flink/checkpoints/
-s3://my-bucket/flink/savepoints/
-```
-
-**주의할 점:**
-- local PV에 checkpoint 저장은 절대 비추 (노드 장애 시 state 손실)
-- S3 권한(IAM Role) 반드시 세팅
-
----
-
-## 로그 및 Config 관리
-
-Flink 설정과 라이브러리는 ConfigMap 또는 initContainer로 관리한다.
-
-### 기본 설정 파일
-- flink-conf.yaml
-- log4j-console.properties
-
-Kubernetes에서는 각종 튜닝 파라미터를 환경 변수 형태로 주입하기도 한다.
-
-**예:**
-```yaml
-- name: FLINK_PROPERTIES
- value: |
- taskmanager.memory.process.size: 4096m
- taskmanager.numberOfTaskSlots: 4
-```
-
----
-
-## Kubernetes Operator 사용 여부
-
-### 1) Flink Kubernetes Operator (공식)
-
-운영 자동화를 위한 강력한 도구.
-
-**기능:**
-- Job 배포 자동화
-- Savepoint 기반 업그레이드 자동화
-- Job 상태 모니터링
-- Restart/Failover 자동 처리
-
-대규모 기업에서 이제는 사실상 표준이다.
-
-**특징:**
-- Application Cluster 운영에 최적화
-- GitOps/ArgoCD와 연계 쉬움
-
----
-
-## 배포 전략 (가장 중요한 부분)
-
-배포 시 아래 두 가지 전략 중 하나를 선택한다.
-
-### 전략 A: Savepoint 기반 롤링 업데이트 (강력 추천)
-
-1. 기존 Job에서 savepoint 생성
-2. 새로운 이미지/코드로 Job 재시작
-3. savepoint에서 state 복구
-4. 문제 발생 시 이전 savepoint로 롤백
-
-**장점:**
-- 정확한 state 유지
-- 지표/세션/집계 모두 유지
-- 진정한 의미의 무중단 업데이트
-
-### 전략 B: Checkpoint 기반 자동 복구
-
-단순 재시작 시 checkpoint로부터 복구하는 방식이지만,
-업데이트에는 savepoint가 훨씬 안정적이다.
-
----
-
-## 장애 대응 전략
-
-Kubernetes 위에서 문제 발생 시 가장 흔한 상황과 해결책:
-
-### ① TaskManager OOM
-- state가 너무 큼 → RocksDB or window 구조 재설계
-- 메모리 부족 → process.size 증가
-- operator chain 해제로 병목 분리
-
-### ② JobManager Restart Loop
-- checkpoint 경로 권한 문제
-- S3 네트워크 오류
-- HA metadata 손상 → cleanup 후 restore 필요
-
-### ③ Backpressure 지속
-- sink 병목 (ClickHouse/JDBC)
-- async I/O 응답 지연
-- parallelism 부족
-- window state 지나치게 큼
-
-### ④ Container가 너무 자주 재시작
-- JVM Heap 부족
-- RocksDB file descriptor 부족
-- CPU throttling (limits 너무 낮음)
-
----
-
-## 실전 아키텍처 예시
-
-```
-Kafka → Flink (K8S Application Cluster)
- ├─ ClickHouse (실시간 지표)
- ├─ Iceberg (Data Lake / 장기 저장)
- └─ Kafka monitoring (알림 / ML Feature Stream)
-
-Checkpoint → S3
-Savepoint → S3/savepoints
-Operator → Flink K8S Operator
-```
-
-대부분의 대규모 데이터 팀은 이 구조 그대로 운영한다.
-
----
-
-## 정리
-
-Flink를 Kubernetes에서 운영할 때 핵심은 Application Cluster 기반으로 Job을 격리하고, Checkpoint와 Savepoint를 S3 같은 내구성 있는 스토리지에 저장하는 것이다. TaskManager는 병렬도와 리소스 요구사항에 따라 스케일되며, JobManager는 Kubernetes HA 방식으로 안정적으로 재시작된다. 배포는 Savepoint 기반 롤링 업데이트가 가장 안전하고, Flink K8S Operator를 사용하면 운영 자동화 수준을 크게 높일 수 있다.
-
----
-
-## 공식 문서 출처
-
-- [Flink on Kubernetes](https://nightlies.apache.org/flink/flink-docs-stable/docs/deployment/resource-providers/overview/)
-- [Kubernetes Native HA](https://nightlies.apache.org/flink/flink-docs-stable/docs/deployment/ha/kubernetes_ha/)
-- [Application vs Session Mode](https://nightlies.apache.org/flink/flink-docs-stable/docs/deployment/overview/)
-- [Checkpoint / Savepoint](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/checkpoints/)
-- [Flink Kubernetes Operator](https://nightlies.apache.org/flink/flink-kubernetes-operator-docs-stable/)
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/kubernetes/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/kubernetes/meta.json"
deleted file mode 100644
index 00e68033..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/kubernetes/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "Flink Kubernetes 운영",
- "slug": "flink-kubernetes",
- "description": "Kubernetes 기반 Flink 운영의 핵심 개념, 배포 전략, 장애 대응을 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "Kubernetes",
- "DevOps"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 7
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 3.5,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/performance-tuning/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/performance-tuning/index.mdx"
deleted file mode 100644
index 9f90f910..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/performance-tuning/index.mdx"
+++ /dev/null
@@ -1,216 +0,0 @@
-# 성능 튜닝
-
-## 전체 그림부터 잡기
-
-Flink 성능 튜닝은 결국 세 가지를 다루는 일임.
-
-- 얼마나 많이 병렬로 돌릴지 (parallelism, 리소스)
-- 각 연산이 얼마나 싸게 돌 수 있게 만들지 (Serialization, State, RocksDB, Window)
-- 시스템이 버티면서 안정적으로 돌아가게 할지 (Checkpoint, Backpressure, Restart)
-
-즉,
-
-**"빨리 + 많이 + 안 터지게"**
-
-이 세 가지를 동시에 맞추는 작업이 성능 튜닝임.
-
----
-
-## 병렬도(parallelism) & 리소스 설계
-
-Flink에서 성능을 가장 크게 바꾸는 레버는 병렬도임.
-
-### 병렬도 기본 개념
-
-- 각 operator는 parallelism을 가짐
-- parallelism N → N개의 subtask가 서로 다른 slot에서 돌게 됨
-- Kafka source의 경우 파티션 수와도 연관됨 (파티션 수가 parallelism보다 작으면 일부 subtask는 할 일 없음)
-
-**병렬도 설계 포인트**
-- source 병렬도: Kafka 파티션 수 기준
-- compute 병렬도: CPU 사용량·state 크기 기준
-- sink 병렬도: 외부 시스템이 감당 가능한 TPS 기준
-
-**실무 감각**
-- CPU 70~80% 근처에서 안정적으로 유지되도록 조정
-- "느리게 도는 task"가 있으면 그 구간 병렬도만 올리는 식으로 부분 조정
-
----
-
-## Operator Chain & Task 배치
-
-Flink는 기본적으로 여러 operator를 하나의 task로 "체인"해서 실행함.
-
-**장점**
-- operator 간 데이터 이동(네트워크/버퍼) 비용 감소
-- GC·스레드 전환 비용 감소
-- 단순 파이프라인에선 체인 덕분에 성능↑
-
-**단점**
-- 특정 operator 하나가 병목이면 같은 체인에 묶인 애들까지 전부 영향
-- 모니터링 시 "어디가 느린지" 구분이 어려워짐
-
-**전략**
-- 가벼운 map/filter 연산은 체인 유지
-- 무거운 window, async I/O, sink 앞뒤는 필요하면 체인 끊기 (disableChaining, startNewChain 등 활용)
-
----
-
-## Serialization 튜닝
-
-직렬화는 성능에 크게 영향 주는 요소인데, 생각보다 많이 간과됨.
-
-**핵심 포인트**
-- 타입을 명확하게 사용 (POJO, Avro, Protobuf 등)
-- `Generic/Map` 같은 구조 남발하면 직렬화 비용↑
-- Kryo 기본 직렬화는 편하지만 비싸고 예측하기 어렵기 때문에, 가능하면 TypeInformation 기반 명시 타입 추천
-
-**전략**
-- schema 고정 가능한 스트림: Avro/Protobuf 사용 고려
-- DataStream/State에 들어가는 타입은 "불필요한 필드" 줄이기
-- 큰 객체를 그대로 state에 넣지 말고 필요한 최소 형태로 변환해서 저장
-
----
-
-## Window & State 크기 관리
-
-성능이 점점 느려지고, checkpoint도 점점 오래 걸린다면 대개 이유는 "state가 비대해졌기 때문"임.
-
-### Window 튜닝 포인트
-- window 길이 줄이기 (너무 긴 윈도우는 state 폭증)
-- key cardinality 관리 (키 수가 너무 많으면 state도 그만큼 증가)
-- 필요 없는 집계/필드를 과감히 제거
-- late data 허용 범위가 과도하게 넓지 않은지 체크
-
-### State 줄이는 기본 전략
-- 오래된 state를 TTL로 날리기 (State TTL 설정)
-- 사실상 필요 없는 key/state는 주기적으로 cleanup
-- MapState/ListState에 무한정 쌓지 말고 "요약된 값"으로 줄이기
-
----
-
-## RocksDB 튜닝 (대규모 state일 때)
-
-대규모 state를 쓴다면 RocksDB를 쓰게 되고, 이때 성능 문제의 80%는 RocksDB 쪽에서 터짐.
-
-**핵심 이슈**
-- compaction 비용
-- 디스크 I/O
-- checkpoint 시 snapshot 크기
-
-**기본 전략**
-- incremental checkpoint 사용 (변경분만 저장)
-- RocksDB block cache·write buffer 크기 조정 (메모리 허용 범위 내에서 키움)
-- local SSD 사용 시 성능 크게 개선
-- 너무 잦은 checkpoint는 RocksDB에 부담 → interval/timeout 적절하게 조정
-
-**체감으로는**
-- checkpoint가 오래 걸리면 RocksDB 튜닝 + checkpoint 주기 조정
-- RocksDB 디렉토리 있는 디스크 I/O 모니터링 필수
-
----
-
-## Checkpoint & Savepoint 성능
-
-Checkpoint는 성능과 안정성의 "절충점"이다.
-
-**너무 자주 하면**
-- job이 checkpoint만 하다 끝나는 느낌
-- 매번 state snapshot 때문에 I/O 폭증
-
-**너무 안 하면**
-- 장애 시 재처리 구간이 너무 길어짐
-- 복구 시간↑
-
-**실무 기준**
-- 지연 허용 여유가 있다면 30~60초 정도 간격에서 시작
-- checkpoint 완료 시간이 interval의 50%를 넘으면 부담이 크다고 보고 튜닝 검토
-- 대규모 state일수록 incremental checkpoint 필수에 가깝게 고려
-
-Savepoint는 성능보단 "배포/업그레이드용"이라, 크기와 생성 시간 정도만 참고.
-
----
-
-## Backpressure 튜닝
-
-성능 튜닝의 최종 보스는 항상 backpressure임.
-
-backpressure는 "어딘가가 느려서 upstream이 밀리는 상태".
-
-**주요 원인**
-- sink가 느림 (DB, ClickHouse, ES, 외부 시스템)
-- async I/O에서 외부 응답 늦음
-- window 연산이 너무 무거움
-- state 접근이 많고 비효율적
-- 네트워크 병목
-
-**해결 방향**
-- 병목 sink의 병렬도 올리기
-- batch size / flush interval 튜닝 (sink connector 설정)
-- async I/O concurrency 늘리기 (동시에 처리 가능한 요청 수)
-- 느린 연산을 앞단에서 필터링해 데이터 양 자체를 줄이기
-- 필요시 operator 체인을 끊어서 병목 구간 분리
-
-**운영 팁**
-- Web UI에서 backpressure 있는 subtask를 먼저 찾기
-- 해당 subtask에 연결된 operator/sink를 중심으로 파헤치기
-
----
-
-## 리소스(CPU/메모리) & Parallelism 전략
-
-성능은 결국 "할당한 리소스를 얼마나 효율적으로 쓰느냐"로 귀결됨.
-
-**전략 느낌**
-- CPU는 60~80% 구간에서 안정적으로 유지되는 선까지 parallelism↑
-- GC가 잦으면: heap 줄이고 task 수를 늘리거나, 상태를 RocksDB로 옮기는 것도 고려
-- TaskManager당 slot 수: 코어 수와 작업 특성을 같이 보고 결정
- (CPU-bound면 코어 수 이하, I/O-bound면 조금 더 높게 설정하는 식)
-
-**주의할 점**
-- 무조건 parallelism을 키운다고 해결되지 않음
- → 외부 시스템이 못 받으면 그냥 병목만 오른쪽으로 이동
-- 병렬도/리소스/외부 시스템 TPS를 "셋트"로 보고 튜닝해야 함
-
----
-
-## 실제로 튜닝할 때의 사고 순서
-
-실무에서 성능 문제를 본다고 하면, 보통 이런 순서로 보면 됨.
-
-1. **지표 확인**
- - 처리량(throughput), 지연(latency), checkpoint 시간, backpressure 여부
-
-2. **어디가 느린지 찾기**
- - Web UI에서 병목 task/operator 확인
-
-3. **병목 유형 판별**
- - CPU 100%? → 계산/직렬화/윈도우/복잡 로직 문제
- - 외부 I/O 대기? → sink/async I/O 문제
- - checkpoint만 오래 걸림? → state/RocksDB 문제
-
-4. **레버 선택**
- - 병렬도 조정
- - operator chain 조정
- - window/state 구조 리팩토링
- - RocksDB/Checkpoint 튜닝
- - sink 설정 튜닝 (batch size, flush interval, retry 등)
-
-이렇게 "지표 → 원인 → 해당 레버" 순서대로 보는 게 제일 깔끔함.
-
----
-
-## 정리
-
-성능 튜닝 관점에서 Flink는 병렬도, state 구조, checkpoint, backpressure 네 가지를 중심으로 보고 있습니다. 병렬도와 리소스를 조정해 처리량을 확보하고, state와 window 크기를 관리해 checkpoint 시간과 RocksDB 부담을 줄입니다. sink와 async I/O 병목은 backpressure로 드러나기 때문에, Web UI와 메트릭을 통해 병목 구간을 찾고 해당 operator의 parallelism, batch size, flush 정책을 조정하는 방식으로 튜닝합니다.
-
----
-
-## 공식 문서 출처
-
-- [Performance Tuning & Monitoring 개요](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/overview/)
-- [Operator Chain](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/operators/overview/)
-- [State & Checkpoint](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/fault-tolerance/state/)
-- [RocksDB State Backend](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/rocksdb/)
-- [Backpressure Monitoring](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/monitoring/back_pressure/)
-- [Resource & Parallelism](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/execution/)
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/performance-tuning/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/performance-tuning/meta.json"
deleted file mode 100644
index f7b95d0a..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/performance-tuning/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "성능 튜닝",
- "slug": "flink-performance-tuning",
- "description": "Flink의 병렬도, Operator Chain, RocksDB, Checkpoint, Backpressure 튜닝 전략을 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "Performance",
- "Tuning"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 6
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 4,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/state-operations/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/state-operations/index.mdx"
deleted file mode 100644
index 4deaebcd..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/state-operations/index.mdx"
+++ /dev/null
@@ -1,249 +0,0 @@
-# State & 운영
-
-## 왜 State Backend가 중요할까?
-
-Flink의 핵심은 "상태(state)를 가진 스트림 처리"임.
-
-여기서 **상태를 어디에 저장하느냐**가 성능, 안정성, 지연, checkpoint 시간 모두를 결정함.
-
-즉,
-- state backend를 어떻게 선택하느냐
-- checkpoint를 어떻게 조정하느냐
-- 재시작 전략을 어떻게 구성하느냐
-이 모든 것이 **운영 품질(안정성)과 성능(레이지/처리량)을 좌우**함.
-
----
-
-## State Backend의 역할
-
-State Backend는 크게 두 가지를 담당함:
-- **상태(state)**를 어디에 저장할 것인가? (메모리 / RocksDB / Changelog 등)
-- **checkpoint/savepoint**를 어떤 방식으로 저장·복구할 것인가?
-
-Flink 앱이 다운돼도
-- "어제까지의 집계 값"
-- "유저별 최근 주문 상태"
-- "Window state"
-
-를 그대로 복구할 수 있는 이유가 바로 backend 때문임.
-
----
-
-## 어떤 Backend가 있는지?
-
-현재 Flink에서 핵심적으로 쓰는 백엔드는 아래 3가지 구조임.
-
-### 1) HashMap State Backend
-
-(이전: Memory / Heap Backend)
-
-**특징**
-- 상태를 메모리에 저장
-- 가장 빠른 backend
-- state가 크지 않을 때 탁월한 선택
-
-**단점**
-- state 크기가 크면 메모리 터지고 checkpoint 시간 폭증
-- 대규모 state를 운영하기 어려움
-
-**적합 케이스**
-- 간단한 count, flag, 소규모 map/list
-- ML inference 라우팅처럼 상태가 거의 없는 job
-
-### 2) RocksDB State Backend
-
-(가장 널리 쓰이는 백엔드)
-
-**특징**
-- 로컬 RocksDB에 state 저장 → 디스크 기반
-- 매우 큰 상태도 안정적으로 처리 가능
-- 장애 복구 안정성 최상
-
-**단점**
-- 메모리 기반보다 느림
-- RocksDB compaction으로 인한 latency variance 발생 가능
-- checkpoint 사이즈가 커질 수 있음
-
-**적합 케이스**
-- 유저별 세션 추적
-- 수십·수백GB 규모 state 관리
-- IoT/주문/로그 스트림처럼 key 규모가 큰 경우
-
----
-
-### 3) Changelog State Backend
-
-(Flink 1.15+ 이후 새로운 추세)
-
-**특징**
-- state 변경 내역(changelog)을 스트리밍처럼 기록
-- checkpoint 부하 감소 (전체 snapshot 대신 변경분만 기록)
-- 복구 속도 향상
-
-현재 많은 기업들이 차세대 표준으로 채택 중임.
-
-**적합 케이스**
-- 대규모 상태 + 빠른 checkpoint 필요할 때
-- 많은 업데이트가 발생하는 state-heavy job
-
----
-
-## 운영에서 가장 중요한 개념: Checkpoint
-
-Checkpoint는 Flink의 "복구 포인트".
-
-**동작 흐름**
-- JobManager가 checkpoint trigger
-- 각 Task가 자신의 state(snapshot) 저장
-- 모든 task가 완료하면 checkpoint 성공
-- 실패하면 해당 시점 이전 checkpoint로 rollback
-
-Checkpoint 설정이 운영난이도를 결정한다고 봐도 됨.
-
----
-
-## Checkpoint 튜닝 포인트
-
-실무에 바로 적용되는 기준으로 정리
-
-### checkpoint interval
-
-*너무 짧으면:* state snapshot 부하 ↑ → 성능 저하
-
-*너무 길면:* 장애 발생 시 재처리 구간 ↑ → 복구 지연
-
-→ 보통 **10초~5분 사이**에서 workload 기반으로 조정함.
-
-### checkpoint timeout
-
-지나치게 짧으면 "checkpoint timeout" 경고가 계속 터짐.
-
-RocksDB state가 크면 오래 걸릴 수 있으므로 별도 조정 필요.
-
-→ 1~5분이 일반적 범위
-
-### min pause between checkpoints
-
-너무 빠르게 연달아 checkpoint가 트리거되지 않게 함.
-
-→ 안정성 확보
-
-### incremental checkpoint (RocksDB)
-
-업데이트된 state의 변경분만 저장하는 방식.
-
-→ checkpoint 속도 크게 향상
-
-→ 하지만 장기적으로 파일 조각화(fragmentation)가 심해질 수 있어 주기적 savepoint 필요
-
----
-
-## Savepoint 운영 전략
-
-Checkpoint는 장애 복구용, Savepoint는 **운영 전략용**임.
-
-**Savepoint 활용 시점**
-- 코드 배포 (Job 업그레이드)
-- parallelism 변경
-- cluster migration
-- 주요 버전 업그레이드
-
-**주의점**
-- state schema 변경 시 호환성 고려
-- 잘못 만들면 state가 날아갈 수 있으므로 staging에서 항상 검증 필요
-- 운영에서는 "savepoint 폴더 보관 정책"도 중요
-
----
-
-## 재시작 전략(Restart Strategy)
-
-Flink job은 실패할 수 있음 → 자동 재시작 정책 필요.
-
-**주요 전략**
-- fixed-delay restart
-- failure-rate restart
-- no restart
-
-일반적으로 실무에서는 아래 구성이 안정적
-- "failure-rate restart + exponential backoff"
-
-**복구 흐름**
-1. 실패
-2. 가장 최근 checkpoint 상태 로드
-3. source offset도 checkpoint 시점으로 돌림
-4. 재시작 후 재처리 → end-to-end exactly-once 유지
-
----
-
-## 운영에서 가장 많이 겪는 문제: Backpressure + Checkpoint 병목
-
-State backend가 RocksDB인 경우 자주 겪는 패턴:
-
-1. RocksDB compaction이 오래 걸림
-2. checkpoint가 지연됨
-3. 다음 checkpoint가 밀림
-4. backpressure 발생
-5. 전체 job의 latency 증가
-
-**이런 경우 해결 방법:**
-- checkpoint interval 늘리기
-- incremental checkpoint 활성화
-- RocksDB block cache / write buffer 튜닝
-- parallelism(병렬도) 조정
-- 특정 operator chaining 해제
-
----
-
-## State Backend 선택 가이드 (실제 운영 기준)
-
-아래 기준이면 거의 90% 커버됨.
-
-### state < 수십 MB
-→ **HashMap Backend** (속도 가장 빠름)
-
-### state 수백 MB~수 GB 단위
-→ **RocksDB Backend** (안정성 최고)
-
-### state 매우 크고 checkpoint 시간이 너무 오래 걸림
-→ **Changelog Backend** 고려 (대규모 streaming use-case)
-
----
-
-## 예시: 주문 이벤트 스트림 기준
-
-**요구사항**
-- 유저별 30분 세션 유지
-- 최근 10개 주문 기억
-- 5분 윈도우 기반 매출 집계
-- 장애 시 이전 상태 그대로 복구
-- 외부 시스템 부하로 checkpoint가 길어짐
-
-**설계**
-- State Backend: RocksDB
-- incremental checkpoint: on
-- checkpoint interval: 30~60초
-- restart strategy: failure-rate + backoff
-- periodic savepoint: 배포 전 수동 생성
-
-이 조합이면 안정성과 성능이 균형 있게 잡힘.
-
----
-
-## 정리
-
-- Flink의 State Backend는 상태를 어디에 저장하고 어떻게 복구할지를 결정하는 핵심 요소.
-- HashMap Backend는 빠르지만 작은 상태에 적합하고, RocksDB Backend는 대규모 상태에 적합
-- 최근에는 Changelog Backend가 checkpoint 성능과 복구 속도를 개선하는 방식으로 사용됨.
-- 운영에서는 checkpoint interval, timeout, incremental checkpoint 설정이 중요하고, savepoint는 코드 배포나 마이그레이션 때 사용함.
-- Job 재시작 시 checkpoint의 state와 source offset을 함께 복구해 exactly-once를 유지하는 구조임.
-
----
-
-## 공식 문서 출처
-
-- [State Backends](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/state_backends/)
-- [RocksDB State Backend](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/rocksdb/)
-- [Checkpointing](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/checkpoints/)
-- [Savepoints](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/savepoints/)
-- [Restart Strategies](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/execution/restart_strategies/)
-- [Backpressure Monitoring](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/monitoring/back_pressure/)
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/state-operations/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/state-operations/meta.json"
deleted file mode 100644
index b80c7f1b..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/state-operations/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "State & 운영",
- "slug": "flink-state-operations",
- "description": "Flink의 State Backend, Checkpoint, Savepoint, 재시작 전략 등 운영 핵심 개념을 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "State",
- "Operations"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 4
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 3.5,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/table-api-sql/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/table-api-sql/index.mdx"
deleted file mode 100644
index 9daa7dae..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/table-api-sql/index.mdx"
+++ /dev/null
@@ -1,252 +0,0 @@
-# Table API & SQL 정복
-
-## Flink Table API & SQL 이란?
-
-Flink에서 고수준 추상화 계층임.
-
-DataStream API로도 다 할 수 있지만, 실무에선 대부분 Table API/SQL로 먼저 풀어보고, 안 되면 DataStream으로 내려가는 흐름이 일반적임.
-
-**정리**
-- "데이터베이스처럼 SQL로 스트림과 배치를 처리할 수 있게 해주는 레이어"
-- 배치와 스트림을 같은 Table/SQL 모델로 다룸
-- 내부적으로는 결국 DataStream으로 컴파일돼서 실행됨
-
-Spark Structured Streaming이 있는 것처럼, Flink에선 Table API/SQL이 그 역할을 담당한다고 보면 됨.
-
----
-
-## 핵심 구성 요소
-
-### TableEnvironment
-
-Flink에서 Table/SQL을 다루는 엔트리 포인트임.
-
-- Table 생성, DDL 실행, SQL 쿼리 실행 담당
-- Connector를 DDL로 선언하고, Catalog/Database/Table을 관리
-- 최종적으로 JobGraph를 만들어서 Flink 클러스터에 제출
-
-보통 이런 흐름으로 씀:
-- EnvironmentSettings 생성 (batch/stream 모드 지정)
-- TableEnvironment.create(...) 호출
-- tableEnv.executeSql("CREATE TABLE ...")
-- tableEnv.sqlQuery(...) 또는 tableEnv.executeSql("INSERT INTO ... SELECT ...")
-
-### EnvironmentSettings
-
-TableEnvironment가 어떻게 동작할지 정하는 설정임.
-
-대부분 이 정도만 구분해도 충분함:
-- inBatchMode()
-- inStreamingMode()
-
-Flink 철학상 배치도 스트림이긴 한데, 최적화 전략과 종료 여부 등에서 차이가 나기 때문에 모드 설정이 필요함.
-
----
-
-## Table, Schema, Catalog 개념
-
-### Table
-
-논리적인 테이블 개념임.
-
-스트림이든 파일이든 Kafka든 결국 Table로 추상화해서 다룸.
-
-**예시**
-- Kafka 토픽 → 주문 이벤트 스트림 테이블
-- S3 Parquet 파일 → 배치 테이블
-- JDBC 테이블 → 차원 테이블(dim table)
-
-### Schema
-
-Table에 대한 컬럼 정의임.
-
-**예시**
-- 필드명, 타입
-- 시간 컬럼 정의 (rowtime, watermark)
-- Primary Key, NOT NULL 같은 제약
-
-이 Schema 정의에 따라 Flink가 타입 체크, 시리얼라이즈, 워터마크 계산 등을 자동으로 처리함.
-
-### Catalog / Database
-
-RDBMS의 Catalog/Schema와 거의 비슷한 느낌임.
-
-- Catalog: 메타데이터 저장소 (Hive Metastore, JDBC, in-memory 등)
-- Database: Catalog 안의 네임스페이스
-
-이걸 잘 써두면:
-- 여러 Job에서 같은 테이블 정의를 재사용할 수 있고
-- SQL Gateway나 BI 도구에서 Flink를 "하나의 DB처럼" 바라볼 수 있음.
-
----
-
-## DDL 기반 Connector 정의
-
-Table API/SQL의 핵심은 DDL로 외부 시스템을 선언하는 것임.
-
-대표 패턴은 "CREATE TABLE … WITH (…)" 형태.
-
-대표적인 Connector 종류:
-
-### FileSystem / Parquet / CSV
-- S3, HDFS, 로컬 파일 등
-- 배치 ETL에 자주 사용
-
-### Kafka
-- topic, format(JSON, Avro, Debezium 등) 지정
-- key/partition metadata도 컬럼으로 가져올 수 있음
-
-### JDBC
-- MySQL, Postgres, MariaDB 등
-- dimension join, 결과 적재 등에 사용
-
-### Iceberg/Hudi 등
-- 레이크하우스 테이블로 쓰기 위한 Connector
-- upsert, snapshot, time-travel 같은 기능 활용 가능
-
-요약하면, "외부 시스템 = 테이블"로 선언만 해두면 이후부터는 그냥 SQL로 join, filter, aggregation 하는 흐름임.
-
----
-
-## 시간 속성 & 워터마크를 SQL로 선언
-
-1번에서 시간·워터마크 개념을 다뤘으니, Table API/SQL에서는 "어떻게 선언하는지"에 집중하면 됨.
-
-### 시간 속성 (Time Attribute)
-
-일반적으로 두 가지를 많이 씀:
-
-**Processing Time 컬럼**
-- `PROCTIME()` 같은 표현으로 정의
-- 운영 편하지만, 정확한 Event-time 집계에는 불리함
-
-**Event Time 컬럼**
-- 기존 timestamp 컬럼에 `WATERMARK FOR ts AS ...` 형태로 정의
-- 예: `WATERMARK FOR ts AS ts - INTERVAL '5' SECOND`
-- 이 정의를 기반으로 Flink가 워터마크를 계산함
-
-핵심은 "DDL에서 시간과 워터마크를 선언하면, 이후 SQL에서 window 함수가 그걸 기반으로 작동한다" 정도로 기억하면 됨.
-
----
-
-## 윈도우(Window) 종류: TUMBLE, HOP, CUMULATE
-
-Table API/SQL에서 자주 나오는 윈도우 세 가지만 확실히 잡으면 면접 대응 충분함.
-
-### TUMBLE (고정 길이 윈도우)
-
-5분 단위, 1시간 단위
-
-겹치지 않는 구간 10:00 ~ 10:05, 10:05 ~ 10:10 …
-
-### HOP (슬라이딩 윈도우)
-
-1분마다 5분 구간 집계
-
-겹치는 구간 [10:00~10:05], [10:01~10:06], [10:02~10:07] …
-
-### CUMULATE (누적 윈도우)
-
-1분 간격으로, 최대 10분까지 누적
-
-예: [10:00~10:01], [10:00~10:02], …, [10:00~10:10]
-
-실무에서 가장 많이 쓰이는 건 TUMBLE/HOP이고, CUMULATE는 실시간 누적 지표에 유용함.
-
----
-
-## StatementSet과 다중 Sink 처리
-
-실제 파이프라인에서는 "동일한 소스 → 여러 목적지" 패턴이 많이 나옴.
-
-**예시: Kafka 주문 이벤트 → Flink → 여러 Sink**
-
-**Sink 1 : 실시간 집계용**
-- 예: 5분 윈도우로 매출/주문 수 집계 → ClickHouse
-- 빠르게 읽기 위해 집계/요약된 형태로 저장
-
-**Sink 2 : 원본/분석용**
-- 주문 이벤트 row level을 날짜/쇼핑몰/카테고리 등으로 파티셔닝해서 Iceberg에 적재
-- 향후 Spark/Flink SQL로 리포트/배치 분석/머신러닝에 활용
-
-**Sink 3 : 모니터링/연동용**
-- "특정 조건(결제 실패, 이상 패턴, 고액 주문 등)"을 만족하는 이벤트만 골라서 모니터링용 Kafka 토픽으로 발행
-- 이 토픽은 알림 시스템, 다른 팀의 서비스들이 다시 소비
-
-이때 StatementSet으로 여러 INSERT를 하나의 Job으로 묶어서 실행할 수 있음.
-
-**요약**
-- TableEnvironment에 StatementSet 생성
-- `addInsertSql` 또는 `addInsert`로 여러 INSERT 추가
-- `execute()` 호출 시 하나의 Flink Job으로 실행됨
-- 소스를 중복으로 읽지 않고 공유하는 구조가 가능해짐
-
----
-
-## UDF (User Defined Function)
-
-SQL로 대부분 해결되지만, 비즈니스 로직이 조금 더 복잡해지면 UDF가 필요해짐.
-
-종류는 크게 세 가지 정도만 기억해도 충분함:
-
-**Scalar Function**
-- 단일 행 → 단일 값
-- 예: 문자열 파싱, 도메인별 커스텀 매핑
-
-**Table Function**
-- 단일 행 → 여러 행
-- 예: JSON 배열을 여러 행으로 풀어내기
-
-**Aggregate Function**
-- 여러 행 → 단일 값
-- 예: 커스텀 집계 로직 (중간 상태를 들고 가는 집계)
-
-UDF를 쓰면 Table API/SQL에서도 꽤 복잡한 도메인 로직을 처리할 수 있고, DataStream으로 내려갈 필요가 줄어듦.
-
----
-
-## Catalog 기반 운영 전략
-
-Table API/SQL을 "단순 코드 레벨 API"로만 쓰지 않고, "데이터베이스처럼" 쓰려면 Catalog를 잘 구성해야 함.
-
-**전략 예시**
-- dev / staging / prod Catalog 분리
-- 각 Catalog 안에 비즈니스 도메인별 Database 구성
-- 공통 차원 테이블, 공통 Kafka 토픽 정의는 재사용 가능하게 관리
-
-이렇게 해두면:
-1. Flink SQL Gateway나 BI 도구에서 Flink를 "하나의 DB"처럼 붙일 수 있고
-2. 팀 내에서 테이블 정의를 공유하기 쉬움
-3. Job 코드 안에서 DDL을 하드코딩하는 비율이 줄어듦
-
----
-
-## 예상 면접 질문 & 답변 스크립트
-
-### Q. Flink Table API/SQL을 어떻게 이해하고 있나요?
-
-"저는 Flink Table API/SQL을 스트림과 배치를 통합해서 다룰 수 있는 고수준 레이어라고 이해하고 있습니다. Kafka, 파일, JDBC 같은 외부 시스템을 테이블로 선언해 두고, 그 위에서 SQL로 윈도우 집계나 조인, ETL을 수행합니다. 내부적으로는 DataStream으로 컴파일되지만, 개발자 입장에서는 대부분의 로직을 SQL/Table 단에서 해결할 수 있는 구조라고 보고 있습니다."
-
----
-
-### Q. DataStream API와 비교했을 때 장단점은?
-
-"DataStream은 세밀한 제어와 복잡한 상태 기반 로직에 강점이 있고, Table API/SQL은 선언적인 방식으로 빠르게 ETL과 집계를 만들 수 있다는 장점이 있습니다. 실무에서는 먼저 Table API/SQL로 모델링하고, 표현이 어려운 고급 패턴이 필요할 때만 DataStream으로 내려가는 것이 유지보수 측면에서 유리하다고 보고 있습니다."
-
----
-
-### Q. 시간 / 워터마크를 Table API/SQL에서 어떻게 다루나요?
-
-"Table DDL에서 이벤트 시간 컬럼과 워터마크를 함께 선언합니다. 예를 들어 timestamp 컬럼에 대해 `WATERMARK FOR ts AS ts - INTERVAL '5' SECOND` 같이 정의하면, Flink가 이 규칙에 따라 워터마크를 계산합니다. 이후 TUMBLE, HOP, CUMULATE 같은 윈도우 함수는 이 시간 컬럼과 워터마크를 기반으로 Event-time 윈도우를 계산하게 됩니다."
-
----
-
-### Q. 여러 Sink로 동시에 보내야 할 때는 어떻게 설계하나요?
-
-"하나의 소스에서 여러 Sink로 나누어 보내야 하는 경우 StatementSet을 사용합니다. 동일한 소스 테이블을 읽는 여러 INSERT를 StatementSet에 등록하고, 한 번의 execute로 하나의 Job으로 실행하면, 소스는 공유하면서도 여러 타겟으로 데이터를 보낼 수 있습니다. 이렇게 하면 리소스 효율과 운영 복잡도 측면에서 유리합니다."
-
----
-
-### Q. UDF는 어떤 경우에 사용하나요?
-
-"도메인 로직이 SQL 표현만으로는 복잡해지는 경우 UDF를 사용합니다. 예를 들면, 특정 포맷의 문자열에서 키 정보를 추출하거나, JSON 구조를 풀어서 여러 행으로 변환하거나, 커스텀 집계 로직이 필요한 경우입니다. Scalar, Table, Aggregate Function을 상황에 맞게 선택해서 Table API/SQL에서 재사용 가능한 함수 단위로 캡슐화하는 식으로 활용합니다."
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/table-api-sql/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/table-api-sql/meta.json"
deleted file mode 100644
index 5e13cc19..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/table-api-sql/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "Table API & SQL 정복",
- "slug": "flink-table-api-sql",
- "description": "Flink의 고수준 추상화 레이어인 Table API/SQL로 스트림과 배치를 통합 처리하는 방법을 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "SQL",
- "Data Engineering"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 2
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 3.5,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\263\240\352\270\211 \352\270\260\353\212\245/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\263\240\352\270\211 \352\270\260\353\212\245/index.mdx"
deleted file mode 100644
index 915abe15..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\263\240\352\270\211 \352\270\260\353\212\245/index.mdx"
+++ /dev/null
@@ -1,261 +0,0 @@
-여기서는 실제 대규모 서비스에서 자주 활용되는 고급 기능만 선별해 정리했다.
-
-CEP, BroadcastState, Async I/O 심화, Watermark 튜닝, Checkpoint 고급 전략까지 모두 포함한다.
-
----
-
-## 큰 그림
-
-대규모 실시간 파이프라인에서는 아래 기능들이 결정적 역할을 한다.
-
-- CEP (Complex Event Processing)
-- Broadcast State (컨트롤/정책 데이터 전파)
-- Async I/O 심화 (외부 DB/모델 호출)
-- Custom Watermark & Event-time 튜닝
-- Side Output 고급 패턴
-- Repartitioning 전략
-- Checkpoint/Savepoint 고급 설계
-- 성능 최적화를 위한 RocksDB & State Schema 설계
-
-아래에 각각의 주요 개념과 실제 적용 사례를 직관적으로 정리했다.
-
----
-
-## 1) CEP — 이벤트 패턴 감지의 핵심
-
-CEP는 "특정 순서·패턴을 만족하는 이벤트 시퀀스 감지" 기능이다.
-
-**예제 상황:**
-- 로그인 → 결제 → 취소 같은 패턴 감지
-- 비정상 행동 패턴 탐지 (봇/사기탐지)
-- 장바구니 담기 → 상품 상세보기 → 구매 전환 흐름 분석
-
-CEP는 다음과 같은 복잡한 조건을 쉽게 표현할 수 있다.
-
-```
-A 이벤트 발생
- AND 5분 내에 B 이벤트
- AND 그 10초 뒤에 C가 발생하면 패턴 성립
-```
-
-**CEP 장점**
-- 복잡한 순서 조건을 간단히 표현
-- event-time 기반으로 정확한 시간 처리 가능
-- 지연 이벤트도 포함될 수 있도록 설정 가능
-
-CEP는 고객 행동 분석이나 Fraud Detection에서 가장 강력한 도구다.
-
----
-
-## 2) Broadcast State — "전사 공통 룰" 공유
-
-BroadcastState는 **모든 TaskManager에게 동일한 정책/룰/설정을 실시간으로 전파**하는 기능이다.
-
-**예제 상황:**
-- 실시간 필터링 규칙 변경
-- ML 모델의 threshold 업데이트
-- 위험 상품 리스트를 모든 TM에게 전달
-- A/B 테스트 구분 로직 배포
-- 광고 추천 룰 변경
-
-**예시:**
-```
-Rules(Kafka Topic) → Broadcast
-Events(Kafka Topic) → KeyedStream
- → Connect → BroadcastProcessFunction
-```
-
-이 구조는 "정책 변경이 잦은 서비스"에서 절대적으로 필요하다.
-
-**대표적인 사용처:**
-- 광고/추천 시스템
-- 실시간 라우팅 서비스
-- 실시간 필터 정책
-- Fraud detection
-
----
-
-## 3) Async I/O 심화 — 외부 시스템 연동 강화
-
-기초 Async I/O는 이미 3단계에서 배웠지만, 실무에서는 훨씬 복잡한 패턴을 다룬다.
-
-**실전 Async I/O 사례:**
-- Redis에서 유저 정보/토큰 조회
-- MySQL에서 상품 카테고리 조회
-- 외부 AI/ML inference API 호출
-- Feature Store 읽기
-- 실시간 가격/재고 API 호출
-
-**고급 설정 포인트:**
-- timeout 설정
-- concurrency (최대 동시 호출 수)
-- capacity (큐 크기)
-- retry/backoff 전략
-- circuit breaker 패턴 적용
-
-**고급 전략:**
-- 병렬도 확장으로 TPS 증가
-- "타입 캐싱"으로 동일 키 반복 조회 방지
-- 병렬 Async I/O → 결과 결합
-
-Async I/O는 잘 쓰면 강력하지만, 잘못 쓰면 backpressure의 주요 원인이 된다.
-
----
-
-## 4) Watermark & Event-time 고급 튜닝
-
-Watermark는 Flink 지연 처리의 핵심.
-
-고급 설정이 필요한 주요 상황은 다음과 같다.
-
-### ① 데이터 지연 정도가 업스트림에 따라 크게 달라질 때
-
-로그 수집 장비나 지역별 네트워크 상황에 따라 event-time 지연이 다를 수 있다.
-
-**해결:**
-- BoundedOutOfOrdernessWatermarks 개별 스트림별로 조정
-- Custom WatermarkGenerator로 상황별 커스텀 정책 구성
-
-### ② 특정 키만 지연이 큰 경우
-
-키별로 watermark 흐름이 달라질 수 있음.
-이때 정밀하게 관리하지 않으면 Window가 너무 늦게 닫힘.
-
-**해결:**
-- Keyed Watermark 관리 → StreamStatus 사용
-- latency histogram 기반 adaptive watermark 생성
-
-### ③ Late Data 다루기
-
-**고급 패턴:**
-- Late event → side output
-- Late event → Iceberg/Hudi에 기록
-- Late event → 보정 파이프라인으로 재전송
-
----
-
-## 5) Side Output 고급 패턴
-
-Side Output은 단순 error 분기만이 아니다.
-
-실전에서는 다음과 같이 적극적으로 활용된다.
-
-- 고위험 이벤트만 별도 Kafka topic
-- 품질 이상치 데이터 스트림 생성
-- 실패 이벤트를 DLQ로 전달
-- A/B 테스트용 스트림 분리
-- 이벤트 라우팅 파이프라인 구성
-
-Side Output은 실제 회사 데이터 파이프라인에서 "서비스별 맞춤 스트림 생성" 기능으로 자주 쓰인다.
-
----
-
-## 6) Repartitioning 전략 (Shuffle, Rebalance, Union)
-
-대규모 데이터 파이프라인에서는 파티셔닝 전략이 직결적으로 성능에 영향을 준다.
-
-**유형:**
-- keyBy — key 기반 파티션
-- rebalance — round-robin (부하 분산)
-- shuffle — 랜덤 분배
-- rescale — split-based partitioning
-- global — 하나의 subtask로 몰아넣기
-
-**전략 선택 기준:**
-- 데이터 편향(key skew) → rebalance or rescale
-- 연산 비용 균등화 필요 → rebalance
-- key 기반 집계 필요 → keyBy
-- 고정 key routing 필요 → shuffle + key mapping
-
-Key skew는 checkpoint 지연과 backpressure의 가장 흔한 원인이므로 파티션 전략은 매우 중요하다.
-
----
-
-## 7) Checkpoint & Savepoint 고급 전략
-
-기본 개념은 이미 배웠으나, 고급 운영에서는 다음을 고려해야 한다.
-
-### ① Checkpoint Alignment 튜닝
-
-alignment timeout, unaligned checkpoint를 통해
-backpressure 상황에서도 checkpoint가 멈추지 않도록 개선.
-
-### ② Incremental + Unified Savepoint 전략
-
-대규모 state에서 savepoint는 시간이 오래 걸리므로
-
-**운영 전략:**
-- 소규모 savepoint: 배포용
-- 대규모 savepoint: 주기적 보관용
-- checkpoint: 장애 복구용
-
-Iceberg/Hudi 기반 Delta Stream을 운영하면 "snapshot 기반 복구"도 가능해진다.
-
----
-
-## 8) RocksDB & State Schema 고급 설계
-
-대규모 state 환경에서 가장 중요한 내용.
-
-**고급 전략:**
-- valueState 대신 mapState 활용해 key 개수 줄이기
-- nested state 피하기 (RocksDB I/O 증가)
-- 큰 state는 "요약(aggregate)" 형태로 변환
-- TTL + cleanup timer로 state 누적 방지
-- RocksDB memory tuning
- - block cache 증가
- - write buffer 증가
- - compaction priority 조정
-
-대규모 기업들은 대개 RocksDB state만 100~500GB를 운영한다.
-
-이 구간에서는 state schema 설계가 성능에 결정적이다.
-
----
-
-## 9) Flink SQL Gateway & Catalog 통합
-
-고급 기능 중 실무에서 가장 빠르게 채택되고 있는 기능.
-
-**SQL Gateway를 사용하면:**
-- Flink job 없이도 SQL로 Iceberg/Hudi 테이블 조회
-- Data Lake 전체를 Flink와 통합
-- streaming + batch 통합 관리
-- 운영/분석/ETL을 모두 SQL로 구현
-
-Catalog 통합은 "Flink = DW 엔진"으로 확장하는 핵심 요소다.
-
----
-
-## 10) ML & 실시간 Feature Engineering
-
-마지막 고급 단계는 ML 실시간 파이프라인과 연결하는 것이다.
-
-**주요 패턴:**
-- 실시간 Feature 계산 → Redis/Feast/ClickHouse
-- raw event → Iceberg → batch training
-- 실시간 inference → Async I/O로 모델 API 호출
-- 온라인/오프라인 feature consistency 유지
-
-Flink는 Spark 대비 **실시간 ML-serving pipeline**에서 강력한 위치를 가진다.
-
----
-
-## 정리
-
-Flink는 단순 ETL 엔진이 아니라 **실시간 데이터 플랫폼의 심장** 역할을 하게 된다.
-
-핵심은 "기능을 많이 아는 것"보다 "어떤 문제에서 어떤 기능을 적용해야 하는지 판단하는 감"을 익히는 것이다.
-
----
-
-## 공식 문서 출처
-
-- [CEP](https://nightlies.apache.org/flink/flink-docs-stable/docs/libs/cep/)
-- [Broadcast State](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/fault-tolerance/broadcast_state/)
-- [Async I/O](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/operators/asyncio/)
-- [Watermarks](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/event-time/generating_watermarks/)
-- [Side Output](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/datastream/operators/process_function/#side-outputs)
-- [Checkpoints & Savepoints](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/checkpoints/)
-- [RocksDB Backend](https://nightlies.apache.org/flink/flink-docs-stable/docs/ops/state/rocksdb/)
-- [Flink SQL Gateway](https://nightlies.apache.org/flink/flink-docs-stable/docs/dev/table/sql-gateway/)
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\263\240\352\270\211 \352\270\260\353\212\245/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\263\240\352\270\211 \352\270\260\353\212\245/meta.json"
deleted file mode 100644
index d4865f29..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\263\240\352\270\211 \352\270\260\353\212\245/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "Flink 고급 기능",
- "slug": "flink-advanced-features",
- "description": "CEP, BroadcastState, Async I/O 심화, Watermark 튜닝 등 대규모 서비스를 위한 고급 기능을 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "CEP",
- "Advanced"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 8
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 3.5,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\270\260\353\263\270\352\260\234\353\205\220/index.mdx" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\270\260\353\263\270\352\260\234\353\205\220/index.mdx"
deleted file mode 100644
index 4731909d..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\270\260\353\263\270\352\260\234\353\205\220/index.mdx"
+++ /dev/null
@@ -1,266 +0,0 @@
-## Flink?
-
-Flink는 "스트림 기반(Stateful Stream Processing) 엔진"
-배치도 할 수 있지만, 본질은 스트림이고 배치조차 "유한한 스트림"으로 보는 철학을 가짐.
-
-**Spark**
-- RDD 기반의 배치 중심 엔진에 스트리밍 기능이 붙어 있는 느낌(특히 Structured Streaming은 micro-batch 느낌이 강함)
-
-**Flink**
-- 처음부터 "무한 스트림 + 상태(State)"를 제대로 처리하려고 설계된 엔진
-
----
-
-## 스트림 vs 배치의 본질적 차이
-
-### 배치 처리
-
-**특징**
-- 배치는 "모든 데이터가 다 모여 있다"는 가정이 깔려 있음.
-- 예를 들어 1년 치 로그 파일을 모아서, 그 위에 쿼리를 한 번 날리는 방식임.
-- 시간은 보통 "컬럼 값"으로만 이용할 뿐, "지금 들어오고 있는 데이터의 시간 흐름" 자체를 신경 쓰지 않음.
-
-**특성**
-- 입력 데이터: 유한(finite)
-- 처리 기준: 대부분 처리 시점(언제 돌리냐)
-- 지연 데이터 개념이 약함: 이미 다 모인 후에 처리하니까
-
-### 스트림 처리
-
-**특징**
-- 스트림은 "데이터가 끝없이 도착한다"는 가정입니다.
-- 예를 들어 IoT 센서, Kafka 로그, 게임 이벤트 스트림처럼 계속 들어옵니다.
-- 여기서 중요한 건 "데이터가 언제 발생했는지(event-time)"와 "언제 도착했는지(ingestion-time)"가 다를 수 있다는 점입니다.
-
-**특성**
-- 입력 데이터: 이론적으로 무한(infinite)
-- 처리 기준: 시간 개념이 핵심 (event-time, processing-time 등)
-- 지연 데이터 처리: 늦게 도착했을 때 어떻게 할 것인지 정책이 필요
-
-본질적으로 이런 차이점이 존재하지만, Flink는 "배치는 유한한 **스트림**이다" 라고 정의한다. 그래서 같은 엔진과 같은 API로 배치와 스트림을 모두 처리할 수 있다.
-
----
-
-## Flink Runtime 구조
-Flink Runtime의 구조는 JobManager, TaskManager, Slot 나누어져 있다.
-각각의 역할은 다음과 같다.
-
-### JobManager
-
-**역할**: 컨트롤 플레인
-
-**책임**
-- Job Graph 생성, 최적화(Logical → Physical Plan)
-- Task 배치와 스케줄링
-- Checkpoint 트리거 및 관리
-- 장애 발생 시 재시작 결정(어디부터, 어느 state로)
-
-### TaskManager
-
-**역할**: 실제 데이터 처리(워커 노드)
-
-내부에 여러 개의 Slot을 가지고 있고, 각 Slot이 하나의 서브태스크(Subtask)를 실행합니다.
-
-### Slot
-
-"논리적인 실행 단위"라고 보면 됩니다.
-
-예를 들어 parallelism 4인 Operator는 4개의 Subtask로 나뉘고, 각 Subtask가 Slot 하나씩을 점유합니다.
-
-하나의 TaskManager가 CPU, memory 리소스를 기반으로 여러 Slot을 가질 수 있습니다.
-
-
-
----
-
-## 실행 흐름 개념
-
-개발자가 Flink Job을 제출하면
-
-- Client가 Job을 JobManager에게 제출
-- JobManager가 JobGraph를 생성 후 ExecutionGraph로 변환
-- ExecutionGraph를 보고 각 TaskManager의 Slot에 Task를 분배
-- TaskManager들이 데이터를 주고받으면서 처리
-
-> 💡 **중요 포인트**
-> - JobManager는 "뇌", TaskManager는 "팔·다리"에 가깝움
-> - Slot 개수와 parallelism 설계가 성능과 비용에 직결됨
-
----
-
-## 시간 개념 : Event-time, Processing-time, Ingestion-time
-
-Flink에서 시간은 스트림 의미를 결정하는 핵심입니다.
-
-### Processing-time
-
-- "현재 머신의 시계" 기준
-- 장점: 구현이 간단. 시스템 시간만 보면 됨
-- 단점: 이벤트가 늦게 도착해도, 시스템 시계 기준으로 윈도우가 닫혀버리므로 "발생 시점 기준 집계"가 어긋날 수 있음
-
-### Event-time
-
-- "이벤트가 실제 발생한 시간" 기준
-- 보통 Kafka 메시지의 timestamp, 로그의 timestamp 필드를 사용
-- 장점: 지연 도착, 네트워크 지연이 있더라도 '발생 시점' 기준으로 정확한 집계가 가능
-- 단점: lateness(지연 도착)에 대한 정책과 watermarks 설계가 필요
-
-### Ingestion-time
-
-- "Flink에 들어온 시점" 기준 (source에서 ingest된 순간)
-- Processing-time의 단점을 조금 보완하지만, Event-time만큼 정교하진 않음
-- 실무에선 Event-time 또는 Processing-time을 주로 사용하고, ingestion-time은 애매한 중간 케이스 정도로 이해해도 됩니다.
-
----
-
-## 워터마크(Watermark)와 지연 데이터(Late Data)
-
-Event-time을 쓴다는 건 "늦게 도착하는 데이터"를 고려하겠다는 뜻입니다.
-
-Flink는 Watermark라는 개념으로 "이 시점까지는 더 이상 예전 이벤트가 들어오지 않았다고 가정해도 된다"를 표현합니다.
-
-### Watermark 기본 개념
-
-Watermark는 보통 "현재까지 본 이벤트의 최대 event-time – 허용 지연 시간" 같은 형태로 설정합니다.
-
-데이터가 보통 5분 이내에 들어온다면 `watermark = maxEventTime – 5 minutes`
-
-Watermark가 특정 윈도우의 끝 시간을 지나치면, 그 윈도우를 "이제 닫아도 된다"고 판단하고 결과를 보냄.
-
-### Late Data(지연 도착 데이터) 처리
-
-Watermark 이후에 들어온, 이미 닫힌 윈도우의 이벤트는 Late Data입니다.
-
-**Late Data에 대한 정책**
-- 버린다 (drop)
-- 별도 side output으로 뺀다
-- 허용 lateness를 추가로 더 준다 (allowedLateness)
-
----
-
-## Stateful Stream Processing 개념
-
-Flink의 가장 큰 특징 중 하나가 "상태(State)를 가진 스트림 처리"입니다.
-
-### State란 무엇인가?
-
-간단히 말해 "연속되는 이벤트들을 처리하면서 필요한 중간값"입니다.
-
-**예시**
-- 유저별 클릭 수 합계
-- 특정 키에 대한 최근 N개 이벤트
-- 윈도우 내의 집계 값
-- 일반 함수형 스트림 처리는 이벤트 하나만 보고 결과를 만들지만, Flink는 "이전까지의 정보"를 상태로 들고 있다가 함께 계산합니다.
-
-### Keyed State / Operator State
-
-**Keyed State**
-- keyBy로 같은 키를 가진 이벤트들이 같은 Subtask로 라우팅됩니다.
-- 각 키별로 독립된 State를 가집니다. (예: userId별 합계, deviceId별 최근 10개 이벤트 등)
-
-**Operator State**
-키 수준이 아니라 오퍼레이터 전체 수준에서 관리하는 상태입니다.
-
-예시: source 연산자가 파일 오프셋을 기억하는 용도 등
-
-### State Backend
-
-이 상태들을 메모리나 RocksDB 등에 저장하는 방식이 State Backend입니다.
-
-**예시**
-- HashMapStateBackend: 메모리에 상태 저장(소규모, 빠름)
-- RocksDBStateBackend(현재는 Changelog 기반 등으로 발전): 디스크에 저장(대량 상태 처리에 적합)
-
----
-
-## Checkpoint와 Savepoint
-
-아마 이 부분은 게임을 좋아하는 유저라면 익숙한 용어일 것 같다. 그 의미랑 비슷하다고 보면된다.
-
-Flink의 내결함성(fault-tolerance) 핵심이 바로 Checkpoint/Savepoint입니다.
-
-### Checkpoint
-
-주기적으로 Stream Job의 상태(state)와 오프셋을 **스냅샷** 떠서 저장함.
-
-주로 장애 복구용의 목적을 가짐.
-
-**예시**
-- Kafka 오프셋, 키별 상태, 윈도우 상태 등을 통째로 스냅샷
-
-**설정 요소**
-- interval: 몇 초/분마다 체크포인트를 찍을지
-- timeout: 얼마나 오래 걸리면 실패로 간주할지
-- 최소 간격(min pause between checkpoints)
-
-### Savepoint
-
-Checkpoint와 비슷하지만 "운영자가 의도적으로" 찍는 스냅샷
-
-Job 업그레이드의 목적을 가지고 수행함.
-
-코드 변경 후 상태를 이어받아 재시작
-
-**특징**
-- 보통 더 안정적인/오래 보관되는 저장소에 둠
-- 호환성 관리 필요 (state schema 변경 등)
-
-### 동작 원리 한 줄 설명
-
-- JobManager가 체크포인트를 트리거
-- 각 TaskManager들이 현재까지 처리한 상태를 State Backend에 저장
-- 전체 Task들이 성공하면 "이 시점까지 완전히 처리된 상태"라는 커밋 포인트처럼 사용
-
----
-
-## Exactly-once의 의미와 Flink 내부 구현 방식
-
-많이들 "Flink는 exactly-once 보장"이라고 말하지만, 면접에서는 이 개념을 정확히 이해하고 있는지를 많이 봅니다.
-
-### Exactly-once의 실제 의미
-
-- "각 이벤트가 논리적으로 정확히 한 번만 처리된 것처럼 보인다"는 의미입니다.
-- 물리적으로는 다시 처리(replay)할 수도 있지만, 상태와 출력이 중복되지 않게 설계하는 것입니다.
-
-### Flink의 접근 방식
-
-**입력 측면**
-- Kafka 같은 source의 오프셋과 state를 함께 체크포인트에 저장합니다.
-- 장애 발생 시, 체크포인트 시점의 state와 오프셋으로 되돌아가서 재실행합니다.
-
-**상태(state) 측면**
-- 상태는 체크포인트 시점 기준으로 일관성(consistency)을 가집니다.
-
-**출력(sink) 측면**
-- Sink가 idempotent하거나, 트랜잭셔널이어야 진정한 end-to-end exactly-once가 됩니다.
-- 예시
- - Kafka sink: 트랜잭션 기반으로 exactly-once 가능
- - JDBC sink: Upsert(Primary Key 기준)로 idempotent하게 만들 수 있음
-
----
-
-## 예상 면접 질문 및 답변
-
-### Flink의 기본 관점
-
-"저는 Flink를 스트림 퍼스트 엔진이라고 이해하고 있습니다. 배치도 처리하지만, 철학적으로는 배치를 유한한 스트림으로 보는 구조입니다. 그래서 Event-time, Watermark, Stateful Processing, Checkpoint 구조가 모두 무한 스트림과 장애 복구를 중심으로 설계되어 있습니다."
-
-### 스트림 vs 배치
-
-"배치는 유한 데이터에 한 번 쿼리하는 느낌이라면, 스트림은 끝없이 들어오는 데이터에 대해 실시간으로, 특히 '언제 발생했는지(event-time)'를 기준으로 집계하는 모델이라고 이해하고 있습니다. Flink는 이 둘을 같은 실행 엔진 위에서 처리해서, 스트림과 배치를 통일된 방식으로 운영할 수 있습니다."
-
-### Runtime 구조
-
-"실행 구조는 JobManager와 TaskManager, 그리고 Slot으로 이해합니다. JobManager는 계획 수립과 스케줄링, 체크포인트 관리를 담당하고, TaskManager는 실제 연산을 수행합니다. parallelism과 Slot 수를 어떻게 설계하느냐가 리소스 효율과 성능에 직결되기 때문에, 이 부분을 튜닝 포인트로 보고 있습니다."
-
-### 시간·워터마크
-
-"시간은 Processing-time, Event-time, Ingestion-time으로 나뉘는데, 실무에서는 Event-time 중심으로 설계합니다. 지연 도착 문제를 해결하기 위해 Watermark를 사용해서 '이 시점 이전 이벤트는 대부분 도착했다'는 기준을 잡고, Late Data에 대해서는 drop이나 side output 같은 정책을 설정합니다."
-
-### 상태와 체크포인트
-
-"Flink의 강점은 상태를 가진 스트림 처리입니다. keyBy 이후 각 키에 대한 상태를 관리하고, 이를 Checkpoint/Savepoint로 스냅샷 떠서 장애 시 동일한 지점에서 재시작할 수 있습니다. Checkpoint는 주기적 장애 복구용, Savepoint는 운영자가 코드 업그레이드나 마이그레이션을 위해 명시적으로 찍는 스냅샷으로 구분해서 사용합니다."
-
-### Exactly-once
-
-"마지막으로 exactly-once는 이벤트가 논리적으로 한 번만 처리된 것처럼 보이게 하는 모델입니다. Flink 내부적으로는 state와 source 오프셋을 함께 체크포인트하고, sink가 트랜잭션 또는 idempotent하게 동작해야 end-to-end exactly-once가 만족됩니다."
diff --git "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\270\260\353\263\270\352\260\234\353\205\220/meta.json" "b/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\270\260\353\263\270\352\260\234\353\205\220/meta.json"
deleted file mode 100644
index f82a606e..00000000
--- "a/posts/\355\224\214\353\247\201\355\201\254-\354\231\204\354\240\204\354\240\225\353\263\265/\352\270\260\353\263\270\352\260\234\353\205\220/meta.json"
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "title": "Flink 기본 개념",
- "slug": "flink-basics",
- "description": "Flink의 스트림 처리 철학, Runtime 구조, 시간 개념, Stateful 처리, Checkpoint, Exactly-once 보장 원리를 정리했습니다.",
- "date": "2025-11-28",
- "category": "Tech",
- "contentType": "essay",
- "tags": [
- "Flink",
- "Stream Processing",
- "Data Engineering"
- ],
- "featured": false,
- "series": {
- "id": "flink-mastery",
- "title": "Flink 완전 정복",
- "order": 1
- },
- "visibility": "private",
- "qualityReview": {
- "philosophy": null,
- "design": null,
- "implementation": null,
- "brandFit": null,
- "clarity": 3.5,
- "structure": 3.5,
- "evidence": 3.5,
- "usefulness": 4,
- "originality": 2.5,
- "polish": 3,
- "reviewedAt": "2026-06-17",
- "notes": ""
- }
-}
diff --git "a/posts/\355\225\250\352\273\230-\354\236\220\353\235\274\352\270\260-\353\246\254\353\267\260/meta.json" "b/posts/\355\225\250\352\273\230-\354\236\220\353\235\274\352\270\260-\353\246\254\353\267\260/meta.json"
index 34ae9432..f6357a23 100644
--- "a/posts/\355\225\250\352\273\230-\354\236\220\353\235\274\352\270\260-\353\246\254\353\267\260/meta.json"
+++ "b/posts/\355\225\250\352\273\230-\354\236\220\353\235\274\352\270\260-\353\246\254\353\267\260/meta.json"
@@ -5,14 +5,9 @@
"date": "2025-12-29",
"category": "Life",
"contentType": "review",
- "tags": [
- "Career",
- "Work",
- "Book",
- "성장"
- ],
+ "tags": ["Career", "Work", "Book", "성장"],
"featured": false,
- "visibility": "private",
+ "visibility": "public",
"qualityReview": {
"philosophy": 3,
"design": 2.5,
@@ -24,7 +19,7 @@
"usefulness": 4,
"originality": 3,
"polish": 3.5,
- "reviewedAt": "2026-07-13",
- "notes": "인용 수치의 원문 페이지와 연구 출처를 보강하기 전까지 비공개"
+ "reviewedAt": "2026-07-20",
+ "notes": "작성자 승인으로 공개 전환. 인용 수치의 원문 페이지와 연구 출처는 후속 점검"
}
}
diff --git a/public/assets/posts/2025-11-28-flink-basics/image.png b/public/assets/posts/2025-11-28-flink-basics/image.png
deleted file mode 100644
index 2d2aebeb..00000000
Binary files a/public/assets/posts/2025-11-28-flink-basics/image.png and /dev/null differ
diff --git a/search/index.ts b/search/index.ts
deleted file mode 100644
index 3f6c64f5..00000000
--- a/search/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { getSearchActions } from './model/get-search-actions';
-export { getRecommendedSearchTerms } from './model/search-recommendations';
diff --git a/search/model/get-search-actions.test.ts b/search/model/get-search-actions.test.ts
deleted file mode 100644
index d678926f..00000000
--- a/search/model/get-search-actions.test.ts
+++ /dev/null
@@ -1,108 +0,0 @@
-import { describe, expect, it, vi } from 'vitest';
-import { getSearchActions } from './get-search-actions';
-
-const mockTrackEvent = vi.fn();
-
-vi.mock('@/infra/analytics/lib/analytics', () => ({
- AnalyticsEvents: {
- click: 'click',
- theme: 'theme',
- view: 'view',
- },
- trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
-}));
-
-describe('getSearchActions', () => {
- it('returns searchable actions from posts', () => {
- const actions = getSearchActions([
- {
- slug: 'redis-basics',
- title: 'Redis Basics',
- category: 'Tech',
- tags: ['Redis', 'Caching'],
- description: 'Redis 기초 개념 정리',
- },
- ]);
-
- const postAction = actions.find((action) => action.id === 'redis-basics');
-
- expect(postAction).toBeDefined();
- expect(postAction?.name).toBe('Redis Basics');
- expect(postAction?.section).toBe('블로그 포스트');
- expect(postAction?.keywords).toContain('Redis');
- });
-
- it('includes section navigation actions for scoped search', () => {
- const actions = getSearchActions([]);
-
- expect(actions.map((action) => action.id)).toEqual([
- 'go-engineering',
- 'go-life',
- 'go-resume',
- ]);
- expect(actions[2].keywords).toContain('이력서');
- });
-
- it('handles missing tag arrays without crashing', () => {
- const actions = getSearchActions([
- {
- slug: 'no-tags',
- title: 'No Tags',
- category: 'Life',
- description: 'Tags missing post',
- tags: undefined,
- },
- ]);
-
- const postAction = actions.find((action) => action.id === 'no-tags');
-
- expect(postAction?.keywords).toBe('No Tags Life Tags missing post');
- });
-
- it('keeps post list order as provided', () => {
- const actions = getSearchActions([
- {
- slug: 'second',
- title: 'B',
- category: 'Tech',
- tags: ['A'],
- description: 'B',
- },
- {
- slug: 'first',
- title: 'A',
- category: 'Tech',
- tags: ['A'],
- description: 'A',
- },
- ]);
-
- expect(actions.map((action) => action.id).slice(3)).toEqual([
- 'second',
- 'first',
- ]);
- });
-
- it('tracks and navigates when action is performed', () => {
- mockTrackEvent.mockClear();
-
- const action = getSearchActions([
- {
- slug: 'redis-basics',
- title: 'Redis Basics',
- category: 'Tech',
- tags: ['Redis'],
- description: 'Redis 기초',
- },
- ]).find((candidate) => candidate.id === 'redis-basics');
-
- action?.perform?.();
-
- expect(mockTrackEvent).toHaveBeenCalledWith('click', {
- target: 'command_palette_result',
- post_slug: 'redis-basics',
- });
- expect(typeof action?.perform).toBe('function');
- expect(() => action?.perform?.()).not.toThrow();
- });
-});
diff --git a/search/model/get-search-actions.ts b/search/model/get-search-actions.ts
deleted file mode 100644
index 82c8458e..00000000
--- a/search/model/get-search-actions.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import { Action } from 'kbar';
-import { FeedData } from '@/blog/model/types';
-import { AnalyticsEvents, trackEvent } from '@/infra/analytics/lib/analytics';
-
-export type SearchablePost = Pick<
- FeedData,
- 'slug' | 'title' | 'category' | 'tags' | 'description'
->;
-
-interface NavigationActionSource {
- id: string;
- name: string;
- href: string;
- keywords: string;
- subtitle: string;
-}
-
-const navigationActions: NavigationActionSource[] = [
- {
- id: 'go-engineering',
- name: 'Tech',
- href: '/engineering',
- keywords: 'Tech Engineering 기술 글 시리즈',
- subtitle: '기술 글과 시리즈 보기',
- },
- {
- id: 'go-life',
- name: 'Life',
- href: '/life',
- keywords: 'Life 회고 에세이 일상',
- subtitle: '회고와 에세이 보기',
- },
- {
- id: 'go-resume',
- name: 'Resume',
- href: '/resume',
- keywords: 'Resume 이력서 경력 프로젝트',
- subtitle: '경력과 프로젝트 보기',
- },
-];
-
-/**
- * 전역 검색을 위한 액션 초기 데이터 생성 함수
- * 블로그 포스트를 검색할 수 있게 액션 객체 배열을 반환합니다.
- */
-export const getSearchActions = (posts: SearchablePost[]): Action[] => {
- const getSearchKeywords = (post: SearchablePost) => {
- const normalizedTags = Array.isArray(post.tags) ? post.tags : [];
-
- return [post.title, post.category, ...normalizedTags, post.description]
- .filter(Boolean)
- .join(' ');
- };
-
- const postActions = posts.map((post) => ({
- id: post.slug,
- name: post.title,
- shortcut: [],
- keywords: getSearchKeywords(post),
- section: '블로그 포스트',
- perform: () => {
- trackEvent(AnalyticsEvents.click, {
- target: 'command_palette_result',
- post_slug: post.slug,
- });
- window.location.assign(`/blog/${post.slug}`);
- },
- subtitle: post.description,
- }));
-
- const sectionActions = navigationActions.map((action) => ({
- id: action.id,
- name: action.name,
- shortcut: [],
- keywords: action.keywords,
- section: '섹션',
- perform: () => {
- trackEvent(AnalyticsEvents.click, {
- target: 'command_palette_section',
- destination: action.href,
- });
- window.location.assign(action.href);
- },
- subtitle: action.subtitle,
- }));
-
- return [...sectionActions, ...postActions];
-};
diff --git a/search/model/search-recommendations.test.ts b/search/model/search-recommendations.test.ts
deleted file mode 100644
index 6c0a7d59..00000000
--- a/search/model/search-recommendations.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import { getRecommendedSearchTerms } from './search-recommendations';
-
-describe('getRecommendedSearchTerms', () => {
- it('returns popular tags ordered by frequency', () => {
- const terms = getRecommendedSearchTerms([
- { tags: ['Redis', '회고'] },
- { tags: ['Redis', 'Flink'] },
- { tags: ['회고'] },
- ]);
-
- expect(terms.slice(0, 3)).toEqual(['Redis', '회고', 'Flink']);
- });
-
- it('falls back to section terms when posts do not have tags', () => {
- expect(getRecommendedSearchTerms([{ tags: [] }])).toEqual([
- 'Tech',
- 'Life',
- 'Resume',
- ]);
- });
-
- it('trims empty tags and respects the requested limit', () => {
- expect(
- getRecommendedSearchTerms([{ tags: [' Redis ', '', 'Flink', '회고'] }], 2)
- ).toHaveLength(2);
- });
-});
diff --git a/search/model/search-recommendations.ts b/search/model/search-recommendations.ts
deleted file mode 100644
index 2fa96d70..00000000
--- a/search/model/search-recommendations.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { FeedData } from '@/blog/model/types';
-
-type SearchRecommendationSource = Pick;
-
-const FALLBACK_RECOMMENDATIONS = ['Tech', 'Life', 'Resume'];
-
-export function getRecommendedSearchTerms(
- posts: SearchRecommendationSource[],
- limit = 5
-): string[] {
- const counts = new Map();
- const firstSeenIndexes = new Map();
- let nextIndex = 0;
-
- posts.forEach((post) => {
- post.tags?.forEach((tag) => {
- const term = tag.trim();
-
- if (term.length === 0) {
- return;
- }
-
- if (!firstSeenIndexes.has(term)) {
- firstSeenIndexes.set(term, nextIndex);
- nextIndex += 1;
- }
-
- counts.set(term, (counts.get(term) ?? 0) + 1);
- });
- });
-
- const recommendations = Array.from(counts.entries())
- .sort(([leftTerm, leftCount], [rightTerm, rightCount]) => {
- if (leftCount !== rightCount) {
- return rightCount - leftCount;
- }
-
- return (
- (firstSeenIndexes.get(leftTerm) ?? 0) -
- (firstSeenIndexes.get(rightTerm) ?? 0)
- );
- })
- .map(([term]) => term)
- .slice(0, limit);
-
- return recommendations.length > 0
- ? recommendations
- : FALLBACK_RECOMMENDATIONS.slice(0, limit);
-}
diff --git a/search/ui/components/CommandPalette/CommandPalette.module.css b/search/ui/components/CommandPalette/CommandPalette.module.css
deleted file mode 100644
index 6edb2dc6..00000000
--- a/search/ui/components/CommandPalette/CommandPalette.module.css
+++ /dev/null
@@ -1,350 +0,0 @@
-.positioner {
- display: flex;
- align-items: flex-start;
- justify-content: center;
- padding: max(var(--space-3), calc(env(safe-area-inset-top) + var(--space-3)))
- var(--space-3) var(--space-3);
- background: rgba(15, 23, 42, 0.28);
- backdrop-filter: blur(10px);
- z-index: var(--z-modal);
-}
-
-.animator {
- max-width: 640px;
- width: min(640px, 100%);
- max-height: min(76vh, 640px);
- display: flex;
- flex-direction: column;
- background: var(--color-bg-primary);
- color: var(--color-text-primary);
- border-radius: var(--radius-lg);
- overflow: hidden;
- box-shadow: 0 18px 40px rgba(15, 23, 42, 0.24);
- border: 1px solid color-mix(in srgb, var(--color-border) 86%, transparent);
-}
-
-.searchWrapper {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: var(--space-3) var(--space-4);
- border-bottom: 1px solid var(--color-border);
- background: color-mix(
- in srgb,
- var(--color-bg-secondary) 58%,
- var(--color-bg-primary)
- );
-}
-
-.search {
- flex: 1;
- min-width: 0;
- padding: 0;
- outline: none;
- border: none;
- box-shadow: none;
- appearance: none;
- -webkit-appearance: none;
- background: transparent;
- color: var(--color-text-primary);
- font-size: var(--text-base);
- font-family: var(--font-sans);
-}
-
-.search:focus,
-.search:focus-visible,
-.search:active {
- outline: none;
- border: none;
- box-shadow: none;
-}
-
-.closeButton {
- width: 2.75rem;
- height: 2.75rem;
- border-radius: var(--radius-full);
- background: color-mix(
- in srgb,
- var(--color-bg-primary) 78%,
- var(--color-bg-secondary)
- );
- border: 1px solid var(--color-border);
- color: var(--color-text-secondary);
- display: inline-flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-}
-
-.closeButton:hover,
-.closeButton:focus-visible {
- border-color: var(--color-toss-blue);
- color: var(--color-toss-blue);
- background: var(--color-bg-primary);
- outline: 2px solid var(--color-toss-blue);
- outline-offset: 2px;
-}
-
-.scopeBar {
- display: flex;
- gap: var(--space-2);
- padding: var(--space-3) var(--space-4);
- overflow-x: auto;
- border-bottom: 1px solid var(--color-border);
-}
-
-.scopeButton {
- min-height: 2.5rem;
- padding: 0 var(--space-4);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-full);
- background: var(--color-bg-primary);
- color: var(--color-text-secondary);
- font-size: var(--text-sm);
- font-weight: var(--font-medium);
- white-space: nowrap;
- cursor: pointer;
- transition:
- background-color var(--duration-150) var(--ease-default),
- border-color var(--duration-150) var(--ease-default),
- color var(--duration-150) var(--ease-default);
-}
-
-.scopeButton:hover,
-.scopeButton:focus-visible {
- border-color: var(--color-toss-blue);
- color: var(--color-toss-blue);
- outline: 2px solid var(--color-toss-blue);
- outline-offset: 2px;
-}
-
-.scopeButtonActive {
- border-color: var(--color-toss-blue);
- background: var(--color-toss-blue);
- color: white;
-}
-
-.scopeButtonActive:hover,
-.scopeButtonActive:focus-visible {
- color: white;
-}
-
-.recommendationPanel {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: var(--space-3) var(--space-4);
- border-bottom: 1px solid var(--color-border);
-}
-
-.recommendationLabel {
- flex-shrink: 0;
- font-size: var(--text-xs);
- font-weight: var(--font-semibold);
- color: var(--color-text-tertiary);
-}
-
-.results {
- max-height: min(62vh, 500px);
- overflow-y: auto;
- overscroll-behavior: contain;
- -webkit-overflow-scrolling: touch;
-}
-
-.sectionHeader {
- padding: var(--space-3) var(--space-5) var(--space-1);
- position: sticky;
- top: 0;
- z-index: 1;
- backdrop-filter: blur(6px);
- background: color-mix(
- in srgb,
- var(--color-bg-primary) 90%,
- var(--color-bg-secondary)
- );
- font-size: var(--text-xs);
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--color-text-tertiary);
- font-weight: var(--font-medium);
-}
-
-.resultItem {
- padding: var(--space-3) var(--space-5);
- min-height: 3.25rem;
- display: flex;
- align-items: center;
- justify-content: space-between;
- cursor: pointer;
- transition: all var(--duration-150) var(--ease-default);
-}
-
-.resultItemActive {
- background: var(--color-bg-secondary);
- border-left: 3px solid var(--color-toss-blue);
- padding-left: calc(var(--space-5) - 3px);
-}
-
-.resultContent {
- display: flex;
- flex-direction: column;
-}
-
-.resultName {
- font-size: var(--text-base);
- font-weight: var(--font-medium);
- color: var(--color-text-primary);
-}
-
-.resultSubtitle {
- font-size: var(--text-sm);
- color: var(--color-text-tertiary);
-}
-
-.resultShortcuts {
- display: flex;
- gap: var(--space-1);
-}
-
-.resultShortcut {
- padding: 2px 6px;
- background: var(--color-bg-secondary);
- border: 1px solid var(--color-border);
- border-radius: var(--radius-sm);
- font-size: var(--text-xs);
- color: var(--color-text-secondary);
-}
-
-.noResultWrapper {
- display: flex;
- flex-direction: column;
- gap: var(--space-3);
- padding: var(--space-5);
-}
-
-.noResultTitle {
- font-size: var(--text-md);
- font-weight: var(--font-semibold);
- color: var(--color-text-primary);
-}
-
-.noResultDescription {
- font-size: var(--text-sm);
- color: var(--color-text-secondary);
- line-height: var(--leading-relaxed);
-}
-
-.suggestionGroup {
- display: flex;
- flex-wrap: wrap;
- gap: var(--space-2);
- margin-top: var(--space-1);
-}
-
-.suggestionButton {
- min-height: 2.75rem;
- border: 1px solid var(--color-border);
- background: var(--color-bg-secondary);
- color: var(--color-text-primary);
- font-size: var(--text-sm);
- font-weight: var(--font-medium);
- border-radius: var(--radius-full);
- padding: 0 var(--space-4);
- cursor: pointer;
- transition: all var(--duration-150) var(--ease-default);
-}
-
-.suggestionButton:hover,
-.suggestionButton:focus-visible {
- border-color: var(--color-toss-blue);
- color: var(--color-toss-blue);
- outline: 2px solid var(--color-toss-blue);
- outline-offset: 2px;
-}
-
-.recoveryActions {
- display: flex;
- gap: var(--space-2);
- margin-top: var(--space-2);
-}
-
-.recoveryButton {
- min-height: 2.75rem;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border: 1px solid var(--color-border);
- border-radius: var(--radius-sm);
- padding: 0 var(--space-4);
- background: transparent;
- color: var(--color-text-secondary);
- font-size: var(--text-sm);
- text-decoration: none;
- cursor: pointer;
- transition: all var(--duration-150) var(--ease-default);
-}
-
-.recoveryButton:hover,
-.recoveryButton:focus-visible {
- border-color: var(--color-toss-blue);
- color: var(--color-toss-blue);
- outline: 2px solid var(--color-toss-blue);
- outline-offset: 2px;
-}
-
-@media (max-width: 767px) {
- .positioner {
- padding: max(
- var(--space-2),
- calc(env(safe-area-inset-top) + var(--space-2))
- )
- var(--space-2) var(--space-2);
- }
-
- .animator {
- width: calc(100vw - 1rem);
- max-height: calc(100dvh - env(safe-area-inset-top) - 1rem);
- border-radius: var(--radius-md);
- }
-
- .searchWrapper {
- padding: var(--space-3);
- }
-
- .scopeBar {
- padding: var(--space-3);
- }
-
- .scopeButton {
- min-height: 2.75rem;
- }
-
- .recommendationPanel {
- align-items: flex-start;
- flex-direction: column;
- gap: var(--space-2);
- padding: var(--space-3);
- }
-
- .search {
- font-size: var(--text-md);
- }
-
- .results {
- max-height: calc(100dvh - env(safe-area-inset-top) - 6.5rem);
- }
-
- .sectionHeader,
- .resultItem {
- padding-left: var(--space-4);
- padding-right: var(--space-4);
- }
-
- .resultItemActive {
- padding-left: calc(var(--space-4) - 3px);
- }
-
- .recoveryActions {
- flex-direction: column;
- }
-}
diff --git a/search/ui/components/CommandPalette/CommandPalette.tsx b/search/ui/components/CommandPalette/CommandPalette.tsx
deleted file mode 100644
index 1c63ddbe..00000000
--- a/search/ui/components/CommandPalette/CommandPalette.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-'use client';
-
-import * as React from 'react';
-import {
- KBarPortal,
- KBarPositioner,
- KBarAnimator,
- KBarSearch,
- KBarResults,
- useKBar,
- useMatches,
-} from 'kbar';
-import styles from './CommandPalette.module.css';
-import type { SearchablePost } from '@/search/model/get-search-actions';
-import { AnalyticsEvents, trackEvent } from '@/infra/analytics/lib/analytics';
-import { getRecommendedSearchTerms } from '@/search/model/search-recommendations';
-
-type SearchScopeId = 'all' | 'tech' | 'life' | 'resume';
-
-interface SearchScope {
- id: SearchScopeId;
- label: string;
- query: string;
-}
-
-interface CommandPaletteProps {
- posts?: SearchablePost[];
-}
-
-const SEARCH_SCOPES: SearchScope[] = [
- { id: 'all', label: '전체', query: '' },
- { id: 'tech', label: 'Tech', query: 'Tech' },
- { id: 'life', label: 'Life', query: 'Life' },
- { id: 'resume', label: 'Resume', query: 'Resume' },
-];
-
-function getActiveSearchScope(normalizedQuery: string): SearchScopeId {
- const matchedScope = SEARCH_SCOPES.find(
- (scope) => scope.query.toLowerCase() === normalizedQuery.toLowerCase()
- );
-
- return matchedScope?.id ?? 'all';
-}
-
-export const CommandPalette = ({ posts = [] }: CommandPaletteProps) => {
- const { query, searchQuery } = useKBar((state) => ({
- searchQuery: state.searchQuery,
- }));
- const normalizedQuery = searchQuery.trim();
- const activeScope = getActiveSearchScope(normalizedQuery);
- const recommendedTerms = React.useMemo(
- () => getRecommendedSearchTerms(posts),
- [posts]
- );
-
- return (
-
-
-
-