From 02fd7b835eff2cfcdfa4f8e7d34dbb8b0f38c96c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9D=80=EC=9A=B0?= Date: Thu, 7 May 2026 23:59:30 +0900 Subject: [PATCH 1/4] fix(blog): stabilize heading anchors and toc links --- blog/model/heading.ts | 64 ++++++++++++++++ blog/services/markdown-parser.test.ts | 32 ++++++++ blog/services/markdown-parser.ts | 73 +++++++++++-------- blog/ui/components/TableOfContents.test.tsx | 29 +++----- .../TableOfContents/TableOfContents.tsx | 43 ++++++----- blog/ui/mdx/components.test.tsx | 37 ++++++++++ blog/ui/mdx/components.tsx | 72 ++++++++++++------ styles/globals.css | 1 + 8 files changed, 259 insertions(+), 92 deletions(-) create mode 100644 blog/model/heading.ts create mode 100644 blog/ui/mdx/components.test.tsx diff --git a/blog/model/heading.ts b/blog/model/heading.ts new file mode 100644 index 0000000..7a79ccc --- /dev/null +++ b/blog/model/heading.ts @@ -0,0 +1,64 @@ +const MARKDOWN_IMAGE_REGEX = /!\[([^\]]*)\]\([^)]*\)/g; +const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\([^)]*\)/g; +const INLINE_CODE_REGEX = /(`+)([^`]*?)\1/g; +const HTML_TAG_REGEX = /<\/?[A-Za-z][^>]*>/g; +const ESCAPED_CHARACTER_REGEX = /\\([\\`*_[\]{}()#+\-.!<>|])/g; +const TRAILING_HEADING_MARKER_REGEX = /\s+#+\s*$/; + +export function normalizeHeadingText(text: string): string { + let normalizedText = text.trim(); + + if (!normalizedText) { + return ''; + } + + normalizedText = normalizedText + .replace(MARKDOWN_IMAGE_REGEX, '$1') + .replace(MARKDOWN_LINK_REGEX, '$1') + .replace(INLINE_CODE_REGEX, '$2') + .replace(HTML_TAG_REGEX, '') + .replace(TRAILING_HEADING_MARKER_REGEX, ''); + + let previousText: string | null = null; + while (normalizedText !== previousText) { + previousText = normalizedText; + normalizedText = normalizedText + .replace(/(\*\*|__)(.*?)\1/g, '$2') + .replace(/(\*|_)(.*?)\1/g, '$2') + .replace(/~~(.*?)~~/g, '$1'); + } + + return normalizedText + .replace(ESCAPED_CHARACTER_REGEX, '$1') + .replace(/\s+/g, ' ') + .trim(); +} + +function buildHeadingSlug(text: string): string { + return normalizeHeadingText(text) + .toLowerCase() + .replace( + /[^\w\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uD7B0-\uD7FF\s-]/g, + '' + ) + .trim() + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); +} + +export function createHeadingIdGenerator() { + const idCounts: Record = {}; + + return (text: string): string | null => { + const baseId = buildHeadingSlug(text); + + if (!baseId) { + return null; + } + + const count = idCounts[baseId] ?? 0; + idCounts[baseId] = count + 1; + + return count === 0 ? baseId : `${baseId}-${count}`; + }; +} diff --git a/blog/services/markdown-parser.test.ts b/blog/services/markdown-parser.test.ts index d8f7ba4..ad6bb34 100644 --- a/blog/services/markdown-parser.test.ts +++ b/blog/services/markdown-parser.test.ts @@ -29,6 +29,38 @@ describe('parseHeadingsFromMdx', () => { }); }); + it('normalizes markdown-heavy headings and skips non-renderable cases', () => { + const mdx = ` +### **๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… (๋กœ์ปฌ ๋ฒค์น˜๋งˆํฌ)** +### [Redis](https://redis.io) \`Pipeline\` +### **๐ŸŽฏ** +\`\`\`md +## code block heading +\`\`\` +### **๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… (๋กœ์ปฌ ๋ฒค์น˜๋งˆํฌ)** +`; + + const headings = parseHeadingsFromMdx(mdx); + + expect(headings).toEqual([ + { + id: '์„ฑ๋Šฅ-์ฆ๋ช…-๋กœ์ปฌ-๋ฒค์น˜๋งˆํฌ', + text: '๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… (๋กœ์ปฌ ๋ฒค์น˜๋งˆํฌ)', + level: 3, + }, + { + id: 'redis-pipeline', + text: 'Redis Pipeline', + level: 3, + }, + { + id: '์„ฑ๋Šฅ-์ฆ๋ช…-๋กœ์ปฌ-๋ฒค์น˜๋งˆํฌ-1', + text: '๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… (๋กœ์ปฌ ๋ฒค์น˜๋งˆํฌ)', + level: 3, + }, + ]); + }); + it('returns an empty array for invalid content', () => { expect(parseHeadingsFromMdx('')).toEqual([]); }); diff --git a/blog/services/markdown-parser.ts b/blog/services/markdown-parser.ts index b43a0cf..5346b15 100644 --- a/blog/services/markdown-parser.ts +++ b/blog/services/markdown-parser.ts @@ -1,20 +1,11 @@ +import { + createHeadingIdGenerator, + normalizeHeadingText, +} from '@/blog/model/heading'; import { getFolderSlug, type TocItem } from '@/blog/services/post-repository'; import fs from 'fs'; import path from 'path'; -// ํ—ค๋”ฉ ํ…์ŠคํŠธ๋ฅผ ID๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜ -function generateHeadingId(text: string): string { - return text - .toLowerCase() - .replace( - /[^\w\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F\uA960-\uA97F\uD7B0-\uD7FF\s-]/g, - '' - ) // ํ•œ๊ธ€, ์˜๋ฌธ, ์ˆซ์ž, ๊ณต๋ฐฑ, ํ•˜์ดํ”ˆ ์ œ์™ธ ์ œ๊ฑฐ (ํŠน์ˆ˜๋ฌธ์ž/์ด๋ชจ์ง€ ์ œ๊ฑฐ) - .trim() - .replace(/\s+/g, '-') // ๊ณต๋ฐฑ์„ ํ•˜์ดํ”ˆ์œผ๋กœ - .replace(/-+/g, '-'); // ์—ฐ์†๋œ ํ•˜์ดํ”ˆ ํ•˜๋‚˜๋กœ -} - // MDX ์†Œ์Šค์—์„œ ํ—ค๋”ฉ ํŒŒ์‹ฑ (๋ Œ๋”๋ง๋œ HTML์ด ์•„๋‹Œ ์›๋ณธ MDX์—์„œ) export function parseHeadingsFromMdx(mdxContent: string): TocItem[] { try { @@ -23,29 +14,51 @@ export function parseHeadingsFromMdx(mdxContent: string): TocItem[] { return []; } - // Regex to match markdown headings (## Heading, ### Heading, etc.) - const headingRegex = /^(#{1,6})\s+(.+)$/gm; const tocItems: TocItem[] = []; - const idCounts: Record = {}; + const nextHeadingId = createHeadingIdGenerator(); + const lines = mdxContent.split(/\r?\n/); + let activeFence: { marker: '`' | '~'; length: number } | null = null; + + for (const line of lines) { + const fenceMatch = /^\s*(`{3,}|~{3,})/.exec(line); - let match; - while ((match = headingRegex.exec(mdxContent)) !== null) { - const level = match[1].length; // Number of # symbols - const text = match[2].trim(); + if (fenceMatch) { + const fenceMarker = fenceMatch[1][0] as '`' | '~'; + const fenceLength = fenceMatch[1].length; + + if (!activeFence) { + activeFence = { + marker: fenceMarker, + length: fenceLength, + }; + } else if ( + activeFence.marker === fenceMarker && + fenceLength >= activeFence.length + ) { + activeFence = null; + } + + continue; + } + + if (activeFence) { + continue; + } + + const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line); + + if (!headingMatch) { + continue; + } - if (!text) continue; + const level = headingMatch[1].length; + const text = normalizeHeadingText(headingMatch[2]); + const id = nextHeadingId(text); - // Generate unique ID - let id = generateHeadingId(text); - if (idCounts[id] !== undefined) { - const count = idCounts[id]; - idCounts[id] = count + 1; - id = `${id}-${count}`; - } else { - idCounts[id] = 1; + if (!text || !id) { + continue; } - // Flat structure - just push all headings tocItems.push({ id, text, diff --git a/blog/ui/components/TableOfContents.test.tsx b/blog/ui/components/TableOfContents.test.tsx index 2c56d52..c299188 100644 --- a/blog/ui/components/TableOfContents.test.tsx +++ b/blog/ui/components/TableOfContents.test.tsx @@ -1,19 +1,14 @@ import { fireEvent, render } from '@testing-library/react'; import { describe, expect, it, vi } from 'vitest'; -import { setupScrollToMock } from '@/shared/testing/dom-mocks'; import { TableOfContents } from './TableOfContents'; describe('TableOfContents', () => { - it('renders items and scrolls/hashes when clicked', () => { - setupScrollToMock(); - + it('renders native hash links and observes headings', () => { const item = { id: 'section-1', text: '์ฒซ๋ฒˆ์งธ ์„น์…˜', level: 2 }; const header = document.createElement('h2'); header.id = item.id; - header.getBoundingClientRect = () => ({ top: 320 } as DOMRect); document.body.appendChild(header); - window.history.replaceState = vi.fn(); const observeSpy = vi.fn(); const disconnectSpy = vi.fn(); @@ -29,23 +24,19 @@ describe('TableOfContents', () => { window.IntersectionObserver = MockIntersectionObserver as unknown as typeof window.IntersectionObserver; - const { getByRole } = render(); + const { container, getByRole } = render(); - const button = getByRole('button', { name: '์ฒซ๋ฒˆ์งธ ์„น์…˜' }); - fireEvent.click(button); + const link = getByRole('link', { name: '์ฒซ๋ฒˆ์งธ ์„น์…˜' }); + fireEvent.click(link); - expect(button).toHaveStyle({ + expect(container.querySelector('nav')).toBeNull(); + expect(document.body.querySelector('nav')).not.toHaveClass('bottom-8'); + expect(document.body.querySelector('nav > div')).not.toHaveClass('h-full'); + expect(document.body.querySelector('ul')).toHaveClass('m-0', 'p-0'); + expect(link).toHaveAttribute('href', `#${item.id}`); + expect(link).toHaveStyle({ fontFamily: 'var(--font-sans-emoji)', }); expect(observeSpy).toHaveBeenCalledWith(header); - expect(window.history.replaceState).toHaveBeenCalledWith( - null, - '', - `#${item.id}` - ); - expect(window.scrollTo).toHaveBeenCalledWith({ - top: 220, - behavior: 'smooth', - }); }); }); diff --git a/blog/ui/components/TableOfContents/TableOfContents.tsx b/blog/ui/components/TableOfContents/TableOfContents.tsx index 0164c0f..6f07677 100644 --- a/blog/ui/components/TableOfContents/TableOfContents.tsx +++ b/blog/ui/components/TableOfContents/TableOfContents.tsx @@ -1,6 +1,7 @@ 'use client'; import { useEffect, useState } from 'react'; +import { createPortal } from 'react-dom'; import { clsx } from 'clsx'; interface TocItem { @@ -15,8 +16,17 @@ interface TableOfContentsProps { export default function TableOfContents({ items }: TableOfContentsProps) { const [activeId, setActiveId] = useState(''); + const [portalRoot, setPortalRoot] = useState(null); useEffect(() => { + setPortalRoot(document.body); + }, []); + + useEffect(() => { + if (items.length === 0) { + return; + } + const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { @@ -38,35 +48,23 @@ export default function TableOfContents({ items }: TableOfContentsProps) { return () => observer.disconnect(); }, [items]); - const handleClick = (id: string) => { - const element = document.getElementById(id); - if (element) { - const yOffset = -100; - const y = - element.getBoundingClientRect().top + window.pageYOffset + yOffset; - window.scrollTo({ top: y, behavior: 'smooth' }); - - // Update URL hash without jumping and without adding to history - window.history.replaceState(null, '', `#${id}`); - } - }; - - if (items.length === 0) return null; + if (items.length === 0 || !portalRoot) return null; - return ( + return createPortal( + , + portalRoot ); } diff --git a/blog/ui/mdx/components.test.tsx b/blog/ui/mdx/components.test.tsx new file mode 100644 index 0000000..cb2b1af --- /dev/null +++ b/blog/ui/mdx/components.test.tsx @@ -0,0 +1,37 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { getMDXComponents } from './components'; + +describe('getMDXComponents', () => { + it('assigns normalized heading ids from rendered text', () => { + const { h3: H3, h4: H4 } = getMDXComponents({}); + + if (!H3 || !H4) { + throw new Error('Expected heading components to be defined'); + } + + render( + <> +

+ + ๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… k6 + +

+

๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… k6

+

ํ•˜์œ„ ์„น์…˜

+ + ); + + const headings = screen.getAllByRole('heading', { + name: '๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… k6', + }); + + expect(headings).toHaveLength(2); + expect(headings[0]).toHaveAttribute('id', '์„ฑ๋Šฅ-์ฆ๋ช…-k6'); + expect(headings[1]).toHaveAttribute('id', '์„ฑ๋Šฅ-์ฆ๋ช…-k6-1'); + expect(screen.getByRole('heading', { name: 'ํ•˜์œ„ ์„น์…˜' })).toHaveAttribute( + 'id', + 'ํ•˜์œ„-์„น์…˜' + ); + }); +}); diff --git a/blog/ui/mdx/components.tsx b/blog/ui/mdx/components.tsx index b73d0a7..7f629de 100644 --- a/blog/ui/mdx/components.tsx +++ b/blog/ui/mdx/components.tsx @@ -1,3 +1,4 @@ +import { createHeadingIdGenerator } from '@/blog/model/heading'; import type { MDXComponents } from 'mdx/types'; import { isValidElement, @@ -55,31 +56,60 @@ function extractText(node: ReactNode): string { return ''; } +function mergeClassName(baseClassName: string, className?: string): string { + return className ? `${baseClassName} ${className}` : baseClassName; +} + export function getMDXComponents(components: MDXComponents): MDXComponents { + const nextHeadingId = createHeadingIdGenerator(); + const createHeading = ( + tag: T, + baseClassName: string + ) => { + const Heading = (props: ComponentPropsWithoutRef) => { + const Tag = tag; + const generatedId = nextHeadingId(extractText(props.children)); + + return ( + + {props.children} + + ); + }; + + Heading.displayName = `MdxHeading(${tag})`; + + return Heading; + }; + return { - h1: (props) => ( -

- {props.children} -

+ h1: createHeading( + 'h1', + 'text-3xl font-bold mt-16 mb-8 text-[var(--color-grey-900)]' ), - h2: (props) => ( -

- {props.children} -

+ h2: createHeading( + 'h2', + 'mt-12 mb-6 text-2xl font-bold text-[var(--color-grey-900)]' ), - h3: (props) => ( -

- {props.children} -

+ h3: createHeading( + 'h3', + 'mt-8 mb-4 text-xl font-bold text-[var(--color-grey-900)]' + ), + h4: createHeading( + 'h4', + 'mt-8 mb-4 text-lg font-bold text-[var(--color-grey-900)]' + ), + h5: createHeading( + 'h5', + 'mt-8 mb-4 text-base font-bold text-[var(--color-grey-900)]' + ), + h6: createHeading( + 'h6', + 'mt-8 mb-4 text-base font-bold text-[var(--color-grey-900)]' ), p: (props) => (

Date: Fri, 8 May 2026 00:00:00 +0900 Subject: [PATCH 2/4] docs(blog): add mac setup draft --- .../index.mdx" | 138 ++++++++++++++++++ .../meta.json" | 16 ++ 2 files changed, 154 insertions(+) create mode 100644 "posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/index.mdx" create mode 100644 "posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/meta.json" diff --git "a/posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/index.mdx" "b/posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/index.mdx" new file mode 100644 index 0000000..bb24e19 --- /dev/null +++ "b/posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/index.mdx" @@ -0,0 +1,138 @@ +## ๋“ค์–ด๊ฐ€๊ธฐ + +์ƒˆ ๋งฅ๋ถ์„ ์„ธํŒ…ํ•  ๋•Œ๋งˆ๋‹ค ํฅ๋ฏธ๋กœ์šด ์ˆœ๊ฐ„์ด ์žˆ์–ด์š”. +๋นˆ ์žฅ๋น„ ์•ž์— ์•‰์œผ๋ฉด, ํ‰์†Œ์— "๋‹ค ํ•„์š”ํ•˜๋‹ค"๊ณ  ์ƒ๊ฐํ•˜๋˜ ๋„๊ตฌ๋“ค ์ค‘ ๋ฌด์—‡์ด ์ •๋ง ๋จผ์ € ๋ณต์›๋ผ์•ผ ํ•˜๋Š”์ง€๊ฐ€ ๋ฐ”๋กœ ๋“œ๋Ÿฌ๋‚˜์š”. + +์ด๋ฒˆ์—๋„ ์ „์ฒด ์Šคํƒ์„ ํ•œ ๋ฒˆ์— ์˜ฌ๋ฆฌ๊ธฐ๋ณด๋‹ค, +์˜ค๋Š˜ ๋ฐ”๋กœ ์ผ์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ฒŒ ํ•ด์ฃผ๋Š” ๊ฒƒ๋“ค๋ถ€ํ„ฐ ๋ณต์›ํ–ˆ์–ด์š”. +๊ฒฐ๊ตญ ์ข‹์€ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ ๋„๊ตฌ ์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ +๋ฆฌ์„œ์น˜, ํŒ๋‹จ, ๊ตฌํ˜„, ๊ธฐ๋ก์˜ ํ๋ฆ„์„ ์–ผ๋งˆ๋‚˜ ๋นจ๋ฆฌ ๋˜์‚ด๋ฆฌ๋А๋ƒ์— ๋‹ฌ๋ ค ์žˆ๋‹ค๊ณ  ๋ณด๊ธฐ ๋•Œ๋ฌธ์ด์—์š”. + +--- + +## ์™œ ๋งฅ๋ถ ์ค‘์‹ฌ์œผ๋กœ ๋‹ค์‹œ ์ •๋ฆฌํ–ˆ๋‚˜ + +๋งฅ๋ถ์„ ๋ฐ”๊พธ๊ฒŒ ๋œ ์ด์œ ๋Š” ๊ฑฐ์ฐฝํ•˜์ง€ ์•Š์•˜์–ด์š”. +๋ฐ์Šคํฌํƒ‘์„ ์ผœ๋Š” ์‹œ๊ฐ„์ด ์ ์  ์ค„์—ˆ๊ณ , +์ž‘์—… ์ž์ฒด๋„ ์ด๋ฏธ ๋งฅ๋ถ ์ค‘์‹ฌ์œผ๋กœ ํ˜๋Ÿฌ๊ฐ€๊ณ  ์žˆ๋‹ค๋Š” ๊ฑธ ๋” ์ด์ƒ ๋ถ€์ •ํ•˜๊ธฐ ์–ด๋ ค์› ์–ด์š”. + +์˜ˆ์ „ ์žฅ๋น„๋กœ๋„ ๊ฐœ๋ฐœ์€ ๊ฐ€๋Šฅํ–ˆ์ง€๋งŒ, +AI ๋„๊ตฌ์™€ IDE, ๋ธŒ๋ผ์šฐ์ €, ํ„ฐ๋ฏธ๋„์„ ํ•จ๊ป˜ ๋„์šฐ๊ธฐ ์‹œ์ž‘ํ•˜๋ฉด ๋ณ‘๋ชฉ์ด ๊ธˆ๋ฐฉ ๋А๊ปด์กŒ์–ด์š”. +๊ธฐ๋‹ค๋ฆฌ๋Š” ์‹œ๊ฐ„์ด ์Œ“์ผ์ˆ˜๋ก ๋ฌธ์ œ๋Š” ์„ฑ๋Šฅ์ด ์•„๋‹ˆ๋ผ ํ๋ฆ„์ด๋ผ๋Š” ์ƒ๊ฐ์ด ๋” ๊ฐ•ํ•ด์กŒ๊ณ , +์ด๋ฒˆ ๊ต์ฒด๋Š” ์†Œ๋น„๋ผ๊ธฐ๋ณด๋‹ค ์ž‘์—… ํ™˜๊ฒฝ์„ ๋‹ค์‹œ ์ •๋ ฌํ•˜๋Š” ์ผ์— ๊ฐ€๊นŒ์› ์–ด์š”. + +--- + +## ์ด๋ฒˆ ๋ณต์›์˜ ๊ธฐ์ค€ + +์ด๋ฒˆ์—๋Š” ์„ธ ๊ฐ€์ง€ ๊ธฐ์ค€์œผ๋กœ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์žก์•˜์–ด์š”. + +1. ๋งค์ผ ์“ฐ๋Š” ๋„๊ตฌ๋งŒ ๋จผ์ € ๋ณต์›ํ•ด์š” +2. ์ƒ๊ฐ์˜ ํ๋ฆ„์„ ๋Š์ง€ ์•Š๋Š” ์ˆœ์„œ๋กœ ์„ค์น˜ํ•ด์š” +3. ํ”„๋กœ์ ํŠธ๋ณ„๋กœ ๋‹ฌ๋ผ์ง€๋Š” ๋Ÿฐํƒ€์ž„์€ ๋’ค๋กœ ๋ฏธ๋ค„์š” + +๊ทธ๋ž˜์„œ ์•ฑ์„ ๋งŽ์ด ๊น”๊ธฐ๋ณด๋‹ค, +"๋ฌด์—‡์ด ์žˆ์–ด์•ผ ๋ฐ”๋กœ ์ผ์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€"๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ฌถ์–ด๋ดค์–ด์š”. + +--- + +## ๊ฐ€์žฅ ๋จผ์ € ๋ณต์›ํ•œ ์•ฑ + +์‹ค์ œ๋กœ ๋จผ์ € ๋ณต์›ํ•œ ์•ฑ์€ ์•„๋ž˜ ์ •๋„์˜ˆ์š”. + +| Tool | Why First | Role | +|---|---|---| +| ChatGPT Atlas | ๋ ˆํผ๋Ÿฐ์Šค๋ฅผ ๋น ๋ฅด๊ฒŒ ๋ชจ์œผ๊ธฐ ์œ„ํ•ด | ๋ฆฌ์„œ์น˜ | +| ChatGPT | ์ƒ๊ฐ ์ •๋ฆฌ์™€ ์„ค๊ณ„ ๊ฒ€ํ† ๋ฅผ ๋ฐ”๋กœ ํ•˜๊ธฐ ์œ„ํ•ด | ํŒ๋‹จ | +| Codex | ๊ตฌํ˜„๊ณผ ๋ฐ˜๋ณต ์ž‘์—…์„ ๋ฐ”๋กœ ์‹œ์ž‘ํ•˜๊ธฐ ์œ„ํ•ด | ์ฝ”๋”ฉ | +| IntelliJ IDEA | ๋ฉ”์ธ IDE๋ฅผ ๋จผ์ € ๋ณต์›ํ•˜๊ธฐ ์œ„ํ•ด | ๊ฐœ๋ฐœ | +| Warp | ๊ฐ€์žฅ ์ž์ฃผ ์“ฐ๋Š” ํ„ฐ๋ฏธ๋„์„ ๋‹ค์‹œ ์˜ฌ๋ฆฌ๊ธฐ ์œ„ํ•ด | ์‹คํ–‰ | +| iTerm | ๋Œ€์ฒด ํ„ฐ๋ฏธ๋„๊ณผ ํ˜ธํ™˜์„ฑ ์ฒดํฌ์šฉ์œผ๋กœ | ๋ฐฑ์—… | +| Notion | ์ž‘์—… ๊ธฐ์ค€๊ณผ ๋ฉ”๋ชจ๋ฅผ ๋ฐ”๋กœ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด | ์ง€์‹ ๊ด€๋ฆฌ | +| Figma | UI์™€ ํ๋ฆ„์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด | ๋””์ž์ธ ๊ฒ€ํ†  | +| KakaoTalk | ๊ธฐ๋ณธ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜์„ ๋Š์ง€ ์•Š๊ธฐ ์œ„ํ•ด | ์†Œํ†ต | +| Magnet | ์ฐฝ ๋ฐฐ์น˜๋ฅผ ๋น ๋ฅด๊ฒŒ ๋ณต์›ํ•˜๊ธฐ ์œ„ํ•ด | ์ž‘์—… ๊ณต๊ฐ„ ์ •๋ฆฌ | + +์ด ๋ชฉ๋ก์„ ๋‹ค์‹œ ๋ณด๋‹ˆ, ์ œ ์—…๋ฌด๋Š” ๊ฒฐ๊ตญ ๋„ค ์ธต์œผ๋กœ ์ •๋ฆฌ๋˜๋”๋ผ๊ณ ์š”. + +- ๋ฆฌ์„œ์น˜: ChatGPT Atlas +- ํŒ๋‹จ: ChatGPT +- ๊ตฌํ˜„: Codex, IntelliJ IDEA, Warp +- ๊ธฐ๋ก๊ณผ ํ˜‘์—…: Notion, Figma, KakaoTalk + +์ƒˆ ์žฅ๋น„๋ฅผ ์ผฐ์„ ๋•Œ ๊ฐ€์žฅ ๋จผ์ € ํ•„์š”ํ•œ ๊ฑด ์™„์ „ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ผ, +์ด ๋„ค ์ธต์ด ๋‹ค์‹œ ์ด์–ด์ง€๋Š” ์ƒํƒœ์˜€์–ด์š”. + +--- + +## ํ„ฐ๋ฏธ๋„๊ณผ CLI๋Š” ์ตœ์†Œํ•œ์œผ๋กœ ๋จผ์ € ๋ณต์›ํ–ˆ๋‹ค + +GUI ์•ฑ๋ณด๋‹ค ๋จผ์ € ์ฒด๊ฐ๋˜๋Š” ๊ฑด ํ„ฐ๋ฏธ๋„ ์ชฝ ์ƒ์‚ฐ์„ฑ์ด์—์š”. +๊ทธ๋ž˜์„œ ์…ธ ํ™˜๊ฒฝ๊ณผ ์ž์ฃผ ์“ฐ๋Š” CLI๋งŒ ๋จผ์ € ๋‹ค์‹œ ์˜ฌ๋ ธ์–ด์š”. + +- Homebrew +- oh-my-zsh +- codex +- gh +- rg +- openjdk +- zsh-autosuggestions +- zsh-syntax-highlighting + +์ด ๊ตฌ์„ฑ์˜ ํ•ต์‹ฌ์€ ๋‹จ์ˆœํ•ด์š”. + +- `Homebrew`๋Š” ๋ณต๊ตฌ์˜ ์‹œ์ž‘์ ์ด์—์š” +- `oh-my-zsh`์™€ ๋‘ ๊ฐœ์˜ zsh ํ”Œ๋Ÿฌ๊ทธ์ธ์€ ํ„ฐ๋ฏธ๋„ ํ”ผ๋กœ๋ฅผ ์ค„์—ฌ์ค˜์š” +- `codex`, `gh`, `rg`๋Š” ๋ฐ”๋กœ ์ƒ์‚ฐ์„ฑ์œผ๋กœ ์ด์–ด์ ธ์š” +- `openjdk`๊นŒ์ง€ ๋จผ์ € ์˜ฌ๋ ค๋‘๋ฉด์„œ ์ตœ์†Œํ•œ์˜ ์‹คํ–‰ ํ™˜๊ฒฝ๋„ ๋น„์›Œ๋‘์ง€ ์•Š์•˜์–ด์š” + +ํŠนํžˆ ์ƒˆ ์žฅ๋น„์—์„œ `rg`์™€ `gh`๊ฐ€ ๋จผ์ € ์žˆ์–ด์•ผ +์ฝ”๋“œ ํƒ์ƒ‰๊ณผ ์ €์žฅ์†Œ ์ž‘์—…์„ ์˜ˆ์ „ ์†๋„๋กœ ๋ฐ”๋กœ ์ด์–ด๊ฐˆ ์ˆ˜ ์žˆ์—ˆ์–ด์š”. + +--- + +## ํฐํŠธ๋„ ์ƒ๊ฐ๋ณด๋‹ค ์ค‘์š”ํ–ˆ๋‹ค + +ํฐํŠธ๋Š” ์ทจํ–ฅ์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” ํ”ผ๋กœ๋„์™€ ์ง๊ฒฐ๋ผ์š”. +์ด๋ฒˆ์— ๋จผ์ € ๋ณต์›ํ•œ ํฐํŠธ๋Š” ์•„๋ž˜ ์„ธ ๊ฐ€์ง€์˜ˆ์š”. + +- Pretendard +- D2Coding +- Fira Code + +ํ•œ๊ธ€ ๋ฌธ์„œ์™€ UI๋ฅผ ์˜ค๋ž˜ ๋ณด๋Š” ์‹œ๊ฐ„์—๋Š” `Pretendard`๊ฐ€ ์•ˆ์ •์ ์ด๊ณ , +์ฝ”๋“œ๋ฅผ ์ฝ์„ ๋•Œ๋Š” `D2Coding`์ด๋‚˜ `Fira Code`๊ฐ€ ๋” ํŽธํ–ˆ์–ด์š”. +์ƒˆ ์žฅ๋น„ ์„ธํŒ… ์ดˆ๋ฐ˜์—๋Š” ์‚ฌ์†Œํ•œ ์ฐจ์ด์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ, +ํ•˜๋ฃจ ์ข…์ผ ๋ณด๋Š” ํ™”๋ฉด์˜ ๋ฐ€๋„๋ฅผ ์ •๋ฆฌํ•ด์ฃผ๋Š” ์š”์†Œ๋ผ์„œ ์˜์™ธ๋กœ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์•˜์–ด์š”. + +--- + +## ์ผ๋ถ€๋Ÿฌ ๋’ค๋กœ ๋ฏธ๋ฃฌ ๊ฒƒ๋“ค + +์ด๋ฒˆ ๋ณต์›์—์„œ ์ผ๋ถ€ ๋„๊ตฌ๋Š” ๋น ์ง„ ๊ฒŒ ์•„๋‹ˆ๋ผ ์˜๋„์ ์œผ๋กœ ๋’ค๋กœ ๋ฏธ๋ค˜์–ด์š”. + +- Node.js, npm, pnpm ๊ฐ™์€ ์›น ํ”„๋กœ์ ํŠธ ๋Ÿฐํƒ€์ž„ +- ์ปจํ…Œ์ด๋„ˆ๋‚˜ ๊ฐ€์ƒํ™” ๋„๊ตฌ +- ๋ธŒ๋ผ์šฐ์ €์™€ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ +- DB ์ ‘์† ๋„๊ตฌ์™€ ํด๋ผ์šฐ๋“œ ํด๋ผ์ด์–ธํŠธ + +์ด๋Ÿฐ ๊ฒƒ๋“ค์€ ์ค‘์š”ํ•˜์ง€ ์•Š์•„์„œ๊ฐ€ ์•„๋‹ˆ๋ผ, +ํ”„๋กœ์ ํŠธ์™€ ๊ณ„์ • ์ƒํƒœ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๋Š” ๋น„์ค‘์ด ์ปค์š”. +๋ฐ˜๋Œ€๋กœ ๋ฆฌ์„œ์น˜, ํŒ๋‹จ, ๊ตฌํ˜„, ๊ธฐ๋ก์— ํ•„์š”ํ•œ ๊ธฐ๋ณธ์ธต์€ +์–ด๋–ค ํ”„๋กœ์ ํŠธ๋ฅผ ํ•˜๋“  ๊ฑฐ์˜ ๋™์ผํ•˜๊ฒŒ ๋ณต์›ํ•ด์•ผ ํ–ˆ์–ด์š”. + +๋นˆ ๋งฅ๋ถ์—์„œ ๋จผ์ € ๋“œ๋Ÿฌ๋‚˜๋Š” ๊ฑด ์ทจํ–ฅ์ด ์•„๋‹ˆ๋ผ ์šฐ์„ ์ˆœ์œ„์˜€์–ด์š”. +์ด๋ฒˆ์— ๋‹ค์‹œ ํ™•์ธํ•œ ๊ฑด, +์ œ๊ฐ€ ๊ฐ€์žฅ ๋จผ์ € ๋ณต์›ํ•˜๋Š” ๋„๊ตฌ๋“ค์ด ๊ฒฐ๊ตญ ์ œ ์ž‘์—… ๋ฐฉ์‹ ์ž์ฒด๋ผ๋Š” ์‚ฌ์‹ค์ด์—ˆ์–ด์š”. + +--- + +## ๋งˆ๋ฌด๋ฆฌ + +์ƒˆ ์žฅ๋น„๋ฅผ ์„ธํŒ…ํ•˜๋ฉด์„œ ์ค‘์š”ํ•œ ๊ฑด +"์–ผ๋งˆ๋‚˜ ๋งŽ์€ ๋„๊ตฌ๋ฅผ ๊น”์•˜๋Š”๊ฐ€"๊ฐ€ ์•„๋‹ˆ์—ˆ์–ด์š”. +"์–ผ๋งˆ๋‚˜ ๋นจ๋ฆฌ ๋‚ด ์ž‘์—… ํ๋ฆ„์„ ๋˜์‚ด๋ ธ๋Š”๊ฐ€"๊ฐ€ ๋” ์ค‘์š”ํ–ˆ์–ด์š”. + +์ด๋ฒˆ ์ •๋ฆฌ๋Š” ์„ค์น˜ ๋ชฉ๋ก์„ ๋‚จ๊ธฐ๊ธฐ ์œ„ํ•œ ๊ธ€์ด๊ธฐ๋„ ํ•˜์ง€๋งŒ, +๋™์‹œ์— ์ œ๊ฐ€ ์–ด๋–ค ์ˆœ์„œ๋กœ ์ผํ•˜๋Š” ์‚ฌ๋žŒ์ธ์ง€ ๋‹ค์‹œ ํ™•์ธํ•œ ๊ธฐ๋ก์ด๊ธฐ๋„ ํ•ด์š”. +๋‹ค์Œ ๋ณต์›์—์„œ๋Š” ๋‚จ์€ ๋Ÿฐํƒ€์ž„๊ณผ ์šด์˜ ๋„๊ตฌ๊นŒ์ง€ ๋” ์ฒด๊ณ„์ ์œผ๋กœ ๋ฌถ์–ด๋ณผ ์ƒ๊ฐ์ด์—์š”. diff --git "a/posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/meta.json" "b/posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/meta.json" new file mode 100644 index 0000000..2c54089 --- /dev/null +++ "b/posts/\352\260\234\353\260\234\355\231\230\352\262\275-\354\264\210\352\270\260\354\204\244\354\240\225/meta.json" @@ -0,0 +1,16 @@ +{ + "title": "์ƒˆ ๋งฅ๋ถ์—์„œ ๊ฐ€์žฅ ๋จผ์ € ๋ณต์›ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ", + "slug": "new-macbook-setup-stack", + "description": "์ƒˆ ๋งฅ๋ถ์„ ์ผ  ๋’ค ์‹ค์ œ๋กœ ๋จผ์ € ๋ณต์›ํ•œ ์•ฑ, CLI, ํฐํŠธ์™€ ๊ทธ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ •๋ฆฌํ•ด์š”.", + "date": "2026-03-12", + "category": "Tech", + "visibility": "private", + "tags": [ + "MacBook", + "Tooling", + "Developer Experience", + "Productivity", + "Workflow" + ], + "featured": false +} From 36016653645e03e932004424d77fec4991d193fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9D=80=EC=9A=B0?= Date: Fri, 8 May 2026 00:00:31 +0900 Subject: [PATCH 3/4] chore(lockfile): refresh optional dependency metadata --- package-lock.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index cd73985..9a3d2d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13987,7 +13987,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, From 378e042ec72d1ae4393481cb5f5574fef60eb65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=B0=95=EC=9D=80=EC=9A=B0?= Date: Fri, 8 May 2026 08:32:37 +0900 Subject: [PATCH 4/4] docs(blog): align ctr post titles and tone --- .../index.mdx" | 76 +++++++++---------- .../meta.json" | 4 +- .../meta.json" | 4 +- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git "a/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/index.mdx" "b/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/index.mdx" index 58f532f..2460f50 100644 --- "a/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/index.mdx" +++ "b/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/index.mdx" @@ -1,68 +1,68 @@ ## ๋“ค์–ด๊ฐ€๊ธฐ -๊ณ ์ˆ˜๋Š” ์žฅ๋น„ ํƒ“์„ ํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ•˜์ง€๋งŒ, ๋กœ์ปฌ์—์„œ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ์˜ฌ๋ฆด ๋•Œ๋งˆ๋‹ค ํŒฌ ์†Œ๋ฆฌ์— ๋†€๋ผ๋ฉด ์ด์•ผ๊ธฐ๊ฐ€ ๋‹ฌ๋ผ์ง„๋‹ค. -์ œ๊ฐ€ ์“ฐ๋Š” ๋…ธํŠธ๋ถ์€ MacBook Air M1 ยท 8GB RAM์ด๋‹ค. Kafka, Flink, Redis, Serving API, ClickHouse ์ •๋„๋Š” ๋„์šธ ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ, ๋ฌธ์ œ๋Š” ๊ทธ ์ˆœ๊ฐ„๋ถ€ํ„ฐ ์‹œ์ž‘๋๋‹ค. +์‹ค์‹œ๊ฐ„ CTR ์ง‘๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ์„ ์ฒ˜์Œ ๊ตฌ์ถ•ํ–ˆ์„ ๋•Œ๋Š” Kafka, Flink, Redis, Serving API, ClickHouse๋ฅผ ๋ชจ๋‘ ๋กœ์ปฌ์—์„œ ๋„์šฐ๋Š” ๊ตฌ์กฐ์˜€๋‹ค. +์‹คํ—˜ ํ™˜๊ฒฝ์€ MacBook Air M1 ยท 8GB RAM์ด์—ˆ๋‹ค. ๊ตฌ์„ฑ ์ž์ฒด๋Š” ๋™์ž‘ํ–ˆ์ง€๋งŒ, ํ•œ ์‚ฌ์ดํด์„ ๋Œ๋ฆด ๋•Œ๋งˆ๋‹ค ๋ฆฌ์†Œ์Šค ํ•œ๊ณ„๊ฐ€ ๋จผ์ € ๋“œ๋Ÿฌ๋‚ฌ๋‹ค. -ํŒŒ์ดํ”„๋ผ์ธ ํ•œ ์‚ฌ์ดํด์„ ๋Œ๋ ค๋ณด๋ ค๋ฉด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋น ์ง์—†์ด ์˜ฌ๋ ค์•ผ ํ–ˆ๊ณ , ๊ทธ๋•Œ๋งˆ๋‹ค ์†Œ์Œ๊ณผ ๋ฐœ์—ด์ด ๋ฐ”๋กœ ๋ณ‘๋ชฉ์œผ๋กœ ๋“œ๋Ÿฌ๋‚ฌ๋‹ค. -๋ฌธ์ œ๋Š” ๋…ธํŠธ๋ถ ์„ฑ๋Šฅ์ด ์•„๋‹ˆ๋ผ ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ ๊ทœ๋ชจ์— ๋น„ํ•ด ๋กœ์ปฌ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ๋„ˆ๋ฌด ๋ฌด๊ฑฐ์› ๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค. +๋ฌธ์ œ๋Š” ๋…ธํŠธ๋ถ ์‚ฌ์–‘ ์ž์ฒด๋ณด๋‹ค ๊ฐœ์ธ ํ”„๋กœ์ ํŠธ์˜ ๊ฒ€์ฆ ๊ทœ๋ชจ์— ๋น„ํ•ด ๋กœ์ปฌ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ๋„ˆ๋ฌด ๋ฌด๊ฑฐ์› ๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค. +Redis์™€ Serving API๊นŒ์ง€ ํ•จ๊ป˜ ์œ ์ง€ํ•˜์ž Flink์— ์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ์ค„์—ˆ๊ณ , ์Šคํ‚ค๋งˆ๋‚˜ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ์ˆ˜์ •ํ•ด์•ผ ํ•  ๊ฒฝ๋กœ๋„ ๋Š˜์–ด๋‚ฌ๋‹ค. -๊ทธ๋ž˜์„œ ์žฅ๋น„๋ฅผ ๋ฐ”๊พธ๋Š” ๋Œ€์‹  ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ์„ ๋‹ค์‹œ ์„ค๊ณ„ํ•ด ๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค. -์ด ๊ธ€์—์„œ๋Š” Redis์™€ Serving API๋ฅผ ๋œ์–ด๋‚ด๋ฉฐ ์–ด๋–ค ๊ธฐ์ค€์œผ๋กœ ๊ตฌ์กฐ๋ฅผ ๋‹จ์ˆœํ™”ํ–ˆ๊ณ , ์‹ค์ œ๋กœ ์„ฑ๋Šฅ์ด ์–ผ๋งˆ๋‚˜ ์ข‹์•„์กŒ๋Š”์ง€ ์ •๋ฆฌํ•œ๋‹ค. +๊ทธ๋ž˜์„œ ์žฅ๋น„๋ฅผ ๋ฐ”๊พธ๋Š” ๋Œ€์‹  ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ์„ ๋‹ค์‹œ ์„ค๊ณ„ํ–ˆ๋‹ค. +์ด ๊ธ€์€ Redis์™€ Serving API๋ฅผ ๋œ์–ด๋‚ด๋Š” ๊ณผ์ •์—์„œ ์–ด๋–ค ๋Œ€์•ˆ์„ ๊ฒ€ํ† ํ–ˆ๊ณ , ClickHouse ์ค‘์‹ฌ ๊ตฌ์กฐ๋กœ ๋ฐ”๊พผ ๋’ค ์„ฑ๋Šฅ๊ณผ ์šด์˜ ๋ณต์žก๋„๊ฐ€ ์–ด๋–ป๊ฒŒ ๋‹ฌ๋ผ์กŒ๋Š”์ง€ ์ •๋ฆฌํ•œ ๊ธฐ๋ก์ด๋‹ค. ![](/images/posts/๋งฅ๋ถ์—์–ด-m1-์ƒํ™œ/d8d42d7debe7.png) --- -## 1. ๋ฌธ์ œ์˜ ์‹œ์ž‘ : โ€œ๋‚ด ๋…ธํŠธ๋ถ ํŒฌ์ด ๋ฉˆ์ถ”์ง€ ์•Š๋Š”๊ฑด ๋‚ด ์‹ค๋ ฅ์ด ๋ถ€์กฑํ•œ ํƒ“์ธ๊ฑธ๊นŒ?โ€ +## 1. ๋ฌธ์ œ์˜ ์‹œ์ž‘: ๋กœ์ปฌ ํ™˜๊ฒฝ์ด ๋จผ์ € ๋ณ‘๋ชฉ์ด ๋๋‹ค -๊ฐœ์ธ ํ”„๋กœ์ ํŠธ๋กœ CTR ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๊ณ  ์ด๊ฒƒ์ €๊ฒƒ ์‹คํ—˜์„ ์ด์–ด๊ฐ€๋˜ ์ค‘, Docker๋ฅผ ์˜ฌ๋ฆด ๋•Œ๋งˆ๋‹ค ๊ฒ๋ถ€ํ„ฐ ๋‚ฌ์–ด์š”. ํ•œ ์‚ฌ์ดํด์„ ๋Œ๋ ค๋ณด๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋น ์ง์—†์ด ์˜ฌ๋ ค์•ผ ํ–ˆ๊ณ , ๊ทธ๋•Œ๋งˆ๋‹ค ํŒฌ ์†Œ๋ฆฌ๊ฐ€ ๊ณผํ•˜๊ฒŒ ์ปค์กŒ์–ด์š”. +๊ฐœ์ธ ํ”„๋กœ์ ํŠธ๋กœ CTR ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•˜๊ณ  ์‹คํ—˜์„ ์ด์–ด๊ฐ€๋˜ ์ค‘, Docker ๊ตฌ์„ฑ์ด ๋จผ์ € ๋ถ€๋‹ด์œผ๋กœ ๋‹ค๊ฐ€์™”๋‹ค. ํ•œ ์‚ฌ์ดํด์„ ๋Œ๋ฆฌ๋ ค๋ฉด ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋น ์ง์—†์ด ์˜ฌ๋ ค์•ผ ํ–ˆ๊ณ , ๊ทธ๋•Œ๋งˆ๋‹ค ๋ฐœ์—ด๊ณผ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ๋น ๋ฅด๊ฒŒ ์ฆ๊ฐ€ํ–ˆ๋‹ค. -๊ธฐ์กด ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜๋Š” ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. +๊ธฐ์กด ์‹œ์Šคํ…œ ์•„ํ‚คํ…์ฒ˜๋Š” ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์—ˆ๋‹ค. ![](/images/posts/๋งฅ๋ถ์—์–ด-m1-์ƒํ™œ/02291ae29fc9.png) -Kafka๋กœ๋ถ€ํ„ฐ ์กฐํšŒ์ˆ˜์™€ ํด๋ฆญ์ˆ˜ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ๋ฐ›์•„ ์ง‘๊ณ„ํ•˜๊ณ  ์ €์žฅํ•˜๋Š” ๊ณผ์ •์—์„œ ClickHouse์™€ Redis์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ณ , ClickHouse๋Š” ๋ถ„์„์šฉ์œผ๋กœ, Redis + Serving API๋Š” ๊ด€๋ จ ๋ถ€์„œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์šฉ๋„๋กœ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค. +Kafka๋กœ๋ถ€ํ„ฐ ์กฐํšŒ์ˆ˜์™€ ํด๋ฆญ์ˆ˜ ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•„ ์ง‘๊ณ„ํ•˜๊ณ  ์ €์žฅํ•˜๋Š” ๊ณผ์ •์—์„œ ClickHouse์™€ Redis์— ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ–ˆ๋‹ค. ClickHouse๋Š” ๋ถ„์„์šฉ์œผ๋กœ, Redis + Serving API๋Š” ๊ด€๋ จ ๋ถ€์„œ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์šฉ๋„๋กœ ๋‘๋ ค ํ–ˆ๋‹ค. -๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑํ•ด ๋ณด๋‹ˆ ์„ธ ๊ฐ€์ง€ Pain Point๊ฐ€ ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค. +๊ทธ๋Ÿฐ๋ฐ ์ด๋ ‡๊ฒŒ ๊ตฌ์„ฑํ•ด ๋ณด๋‹ˆ ์„ธ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋‹ค. -1. **๋ฆฌ์†Œ์Šค ๋ถ€์กฑ** : Redis + Serving API ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ฐจ์ง€ํ•˜๋Š” Memory์™€ CPU ๋•Œ๋ฌธ์— ์ •์ž‘ ์ค‘์š”ํ•œ Flink Job์ด OOM์œผ๋กœ ์ฃฝ๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. -2. **๊ด€๋ฆฌ ํฌ์ธํŠธ ์ฆ๊ฐ€** : ์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ณ€๊ฒฝ์œผ๋กœ ์Šคํ‚ค๋งˆ๊ฐ€ ๋ฐ”๋€Œ๋ฉด Flink๋ฅผ ์ˆ˜์ •ํ•˜๊ณ , Redis ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•œ ๋’ค, API ์ฝ”๋“œ๊นŒ์ง€ ์ˆ˜์ •ํ•ด์•ผ ํ•ด์„œ ํ˜ผ์ž ๊ฐœ๋ฐœํ•˜๋Š”๋ฐ๋„ Context Switching ๋น„์šฉ์ด ๋„ˆ๋ฌด ์ปธ์Šต๋‹ˆ๋‹ค. -3. **๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ** : ๋กœ์ปฌ ํ™˜๊ฒฝ์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  Flink โ†’ Redis โ†’ API ๊ณผ์ •์—์„œ ๋ถˆํ•„์š”ํ•œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ๋น„์šฉ์ด ๊ณ„์† ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. +1. **๋ฆฌ์†Œ์Šค ๋ถ€์กฑ**: Redis + Serving API ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์ฐจ์ง€ํ•˜๋Š” ๋ฉ”๋ชจ๋ฆฌ์™€ CPU ๋•Œ๋ฌธ์— ์ •์ž‘ ์ค‘์š”ํ•œ Flink Job์ด OOM์œผ๋กœ ์ฃฝ๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์—ˆ๋‹ค. +2. **๊ด€๋ฆฌ ํฌ์ธํŠธ ์ฆ๊ฐ€**: ์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋ณ€๊ฒฝ์œผ๋กœ ์Šคํ‚ค๋งˆ๊ฐ€ ๋ฐ”๋€Œ๋ฉด Flink๋ฅผ ์ˆ˜์ •ํ•˜๊ณ , Redis ๋ฐ์ดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•œ ๋’ค, API ์ฝ”๋“œ๊นŒ์ง€ ์ˆ˜์ •ํ•ด์•ผ ํ–ˆ๋‹ค. +3. **๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ**: ๋กœ์ปฌ ํ™˜๊ฒฝ์ž„์—๋„ Flink -> Redis -> API ๊ฒฝ๋กœ์—์„œ ๋ถˆํ•„์š”ํ•œ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™” ๋น„์šฉ์ด ๊ณ„์† ๋ฐœ์ƒํ–ˆ๋‹ค. -๊ฐ€๋งŒํžˆ ์ƒ๊ฐํ•ด ๋ณด๋‹ˆ API๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฃผ์š” ๊ณ ๊ฐ์„ ML ํŒ€์œผ๋กœ ๊ฐ€์ •ํ–ˆ๋Š”๋ฐ, ๋งŒ์•ฝ ์‚ฌ๋‚ด ํŒ€์—์„œ ClickHouse๋กœ ์ง์ ‘ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด Redis์™€ Serving API๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค๋Š” ํŒ๋‹จ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. +API๋ฅผ ์กฐํšŒํ•˜๋Š” ์ฃผ์š” ์‚ฌ์šฉ์ž๋ฅผ ML ํŒ€์œผ๋กœ ๊ฐ€์ •ํ–ˆ๋Š”๋ฐ, ์‚ฌ๋‚ด ํŒ€์—์„œ ClickHouse๋กœ ์ง์ ‘ ์กฐํšŒํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด Redis์™€ Serving API๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ๋‹ค. -์ด ์ƒ๊ฐ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋˜ ์ค‘, ๋Œ€๊ธฐ์—…์—์„œ ๊ทผ๋ฌดํ•˜๋Š” ์นœ๊ตฌ์—๊ฒŒ ๊ตฌ์ถ•ํ•œ ์‹œ์Šคํ…œ์— ๋Œ€ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์„ ๊ธฐํšŒ๊ฐ€ ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ณผ์ •์—์„œ Redis์™€ Serving API๊ฐ€ ๊ผญ ํ•„์š”ํ•œ์ง€, ์ด ์‹œ์Šคํ…œ์—์„œ ๊ณ ๋ คํ•˜๋Š” ์ƒํ’ˆ ์ˆ˜๋Š” ์–ผ๋งˆ์ธ์ง€, Redis HashTable ํŠน์„ฑ์ƒ ๋ฆฌ์‚ฌ์ด์ง• ์ž‘์—…์ด๋‚˜ ํ‚ค ์—…๋ฐ์ดํŠธ๊ฐ€ ์ผ์–ด๋‚  ๋•Œ ์กฐํšŒ ์š”์ฒญ์ด ๋™์‹œ์— ๋ฐœ์ƒํ•œ๋‹ค๋ฉด ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋Š”์ง€ ๋“ฑ ์—ฌ๋Ÿฌ ์งˆ๋ฌธ์„ ํ†ตํ•ด Redis์˜ ์กด์žฌ ์ž์ฒด์— ๋Œ€ํ•œ ๊นŠ์€ ๊ณ ๋ฏผ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ์‹œ๊ฐ„์„ ๊ฐ€์กŒ์Šต๋‹ˆ๋‹ค. +์ด ์ƒ๊ฐ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋˜ ์ค‘, ๋Œ€๊ธฐ์—…์—์„œ ๊ทผ๋ฌดํ•˜๋Š” ์นœ๊ตฌ์—๊ฒŒ ๊ตฌ์ถ•ํ•œ ์‹œ์Šคํ…œ์— ๋Œ€ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์„ ๊ธฐํšŒ๊ฐ€ ์ƒ๊ฒผ๋‹ค. ๊ทธ ๊ณผ์ •์—์„œ Redis์™€ Serving API๊ฐ€ ๊ผญ ํ•„์š”ํ•œ์ง€, ์ด ์‹œ์Šคํ…œ์—์„œ ๊ณ ๋ คํ•˜๋Š” ์ƒํ’ˆ ์ˆ˜๋Š” ์–ผ๋งˆ์ธ์ง€, Redis HashTable ํŠน์„ฑ์ƒ ๋ฆฌ์‚ฌ์ด์ง• ์ž‘์—…์ด๋‚˜ ํ‚ค ์—…๋ฐ์ดํŠธ๊ฐ€ ์ผ์–ด๋‚  ๋•Œ ์กฐํšŒ ์š”์ฒญ์ด ๋™์‹œ์— ๋ฐœ์ƒํ•˜๋ฉด ์–ด๋–ค ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ๋Š”์ง€ ์งˆ๋ฌธ์„ ๋ฐ›์•˜๋‹ค. ์ด ํ”ผ๋“œ๋ฐฑ์„ ๊ณ„๊ธฐ๋กœ Redis์˜ ์กด์žฌ ์ด์œ ๋ฅผ ๋‹ค์‹œ ๊ฒ€ํ† ํ–ˆ๋‹ค. -์ œ๊ฐ€ ์„ค๊ณ„ํ•œ ์˜๋„์— ๋Œ€ํ•ด ์„œ๋กœ ๋…ผ์˜ํ–ˆ๊ณ , ๊ฒฐ๋ก ์ ์œผ๋กœ ๋‘ ๊ฐ€์ง€ ๋Œ€์•ˆ์„ ์ œ์‹œํ•ด ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. +์„ค๊ณ„ ์˜๋„๋ฅผ ํ•จ๊ป˜ ๋…ผ์˜ํ–ˆ๊ณ , ๊ฒฐ๋ก ์ ์œผ๋กœ ๋‘ ๊ฐ€์ง€ ๋Œ€์•ˆ์ด ๋‚˜์™”๋‹ค. 1. **์ง€์—ฐ ์—†์ด ์ ์žฌ ๊ฐ€๋Šฅํ•œ ์Šคํ† ๋ฆฌ์ง€ ํฌ๋งท ์‚ฌ์šฉ** (์˜ˆ: Iceberg) 2. **ClickHouse์˜ Materialized View ํ™œ์šฉ** (์•ฝ์–ด๋กœ MV๋ผ๊ณ  ๋งŽ์ด ์‚ฌ์šฉํ•จ.) --- -## 2. ์˜ˆ์ธก๊ณผ ์„ค๊ณ„ : Redis + Serving API๋ฅผ ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค! +## 2. ๊ฐ€์„ค๊ณผ ์„ค๊ณ„: Redis + Serving API๋ฅผ ์ œ๊ฑฐํ•œ๋‹ค -๋ฌผ๋ก  Redis๋Š” 1ms ์ˆ˜์ค€์œผ๋กœ ๋น ๋ฅด์ง€๋งŒ, ์ œ ํ”„๋กœ์ ํŠธ์˜ ๋ชฉํ‘œ์™€ ์ œ์•ฝ์„ ๋‹ค์‹œ ์ƒ๊ฐํ•ด๋ดค์Šต๋‹ˆ๋‹ค. +๋ฌผ๋ก  Redis๋Š” 1ms ์ˆ˜์ค€์œผ๋กœ ๋น ๋ฅด์ง€๋งŒ, ํ”„๋กœ์ ํŠธ์˜ ๋ชฉํ‘œ์™€ ์ œ์•ฝ์„ ๋‹ค์‹œ ์ƒ๊ฐํ–ˆ๋‹ค. - ๋ชฉํ‘œ: ์ดˆ๋‹น 10๋งŒ๊ฑด์˜ ์ด๋ฒคํŠธ๋ฅผ ์œ ์‹ค ์—†์ด ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ ๊ตฌ์ถ• - ์ œ์•ฝ: MacBook Air M1, 8GB ๊ธฐ๋ฐ˜์˜ ๋กœ์ปฌ ๊ฐœ๋ฐœํ™˜๊ฒฝ, ๋ฆฌ์†Œ์Šค ์ตœ์†Œํ™” -- ๊ฐ€์„ค: Redis ๋Œ€์‹  ๋Œ€์•ˆ์„ ์‚ฌ์šฉํ•ด๋„ ์‹ค์‹œ๊ฐ„์„ฑ์„ ์ถฉ๋ถ„ํžˆ ๋ณด์žฅํ•˜๋ฉด์„œ๋„ ๋ณต์žก๋„๋ฅผ ๋‚ฎ์ถœ ์ˆ˜ ์žˆ๋‹ค. +- ๊ฐ€์„ค: Redis ๋Œ€์‹  ๋‹ค๋ฅธ ์ €์žฅ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•ด๋„ ์‹ค์‹œ๊ฐ„์„ฑ์„ ์ถฉ๋ถ„ํžˆ ๋ณด์žฅํ•˜๋ฉด์„œ ๋ณต์žก๋„๋ฅผ ๋‚ฎ์ถœ ์ˆ˜ ์žˆ๋‹ค. -๋˜ํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ด€์ ์—์„œ ๋ณด๋ฉด, Serving Layer์™€ Storage Layer๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ด€ํ–‰์ด ์˜คํžˆ๋ ค ์‹œ์Šคํ…œ ์ดˆ๊ธฐ ๋‹จ๊ณ„์—์„œ๋Š” ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๊ฒฐํ•ฉ๋„๋งŒ ๋†’์ธ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. +๋˜ํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ด€์ ์—์„œ ๋ณด๋ฉด, Serving Layer์™€ Storage Layer๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๊ด€ํ–‰์ด ์‹œ์Šคํ…œ ์ดˆ๊ธฐ ๋‹จ๊ณ„์—์„œ๋Š” ๋ถˆํ•„์š”ํ•˜๊ฒŒ ๊ฒฐํ•ฉ๋„๋งŒ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค๊ณ  ๋ดค๋‹ค. --- -## 3. ๋Œ€์•ˆ ์„ ํƒ : Materialized VIew ํ™œ์šฉ +## 3. ๋Œ€์•ˆ ์„ ํƒ: ClickHouse Materialized View -์ƒˆ๋กœ์šด ์Šคํ† ๋ฆฌ์ง€ ํฌ๋งท์„ ๋„์ž…ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์ง€๋งŒ, ์ œ์•ฝ ์กฐ๊ฑด์„ ํ•ด๊ฒฐํ•˜๊ณ ์ž ํ•˜๋Š” ๋งˆ์Œ์ด ์ปธ๊ธฐ์— ClickHouse๋ฅผ ํ™œ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ์Šต๋‹ˆ๋‹ค. +์ƒˆ๋กœ์šด ์Šคํ† ๋ฆฌ์ง€ ํฌ๋งท์„ ๋„์ž…ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์ง€๋งŒ, ์šฐ์„  ๋กœ์ปฌ ์ œ์•ฝ์„ ์ค„์ด๋Š” ๊ฒƒ์ด ๋” ์ค‘์š”ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๊ธฐ์กด ๊ตฌ์„ฑ์— ์ด๋ฏธ ํฌํ•จ๋œ ClickHouse๋ฅผ ํ™œ์šฉํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ–ˆ๋‹ค. -ClickHouse์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์ธ `Materialized View`๋ฅผ ํ™œ์šฉํ•˜๋ฉด, ๋ณ„๋„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ง ์—†์ด๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋Š” ์ˆœ๊ฐ„ โ€œ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ ์ƒํƒœโ€๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฆ‰, **ClickHouse ์ž์ฒด๊ฐ€ ์บ์‹œ ์„œ๋ฒ„ ์—ญํ• ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•˜๊ฒŒ ๋˜๋Š” ๊ฒƒ**์ž…๋‹ˆ๋‹ค. +ClickHouse์˜ `Materialized View`๋ฅผ ํ™œ์šฉํ•˜๋ฉด ๋ณ„๋„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋กœ์ง ์—†์ด๋„ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด์˜ค๋Š” ์ˆœ๊ฐ„ โ€œ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ ์ƒํƒœโ€๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰, **ClickHouse ์ž์ฒด๊ฐ€ ์บ์‹œ ์„œ๋ฒ„ ์—ญํ• ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ตฌ์กฐ**๊ฐ€ ๋œ๋‹ค. ### ๋ฐ์ดํ„ฐ ํ๋ฆ„ ![](/images/posts/๋งฅ๋ถ์—์–ด-m1-์ƒํ™œ/97d2416a9a38.png) -### **์ ์šฉํ•œ 3๊ฐ€์ง€ View ์ „๋žต** +### ์ ์šฉํ•œ 3๊ฐ€์ง€ View ์ „๋žต 1. **`ctr_latest_view`**: Redis์˜ Key-Value ์กฐํšŒ๋ฅผ ๋Œ€์ฒด. ํ•ญ์ƒ ์ตœ์‹  CTR ์ƒํƒœ๋งŒ ์œ ์ง€ (`ReplacingMergeTree`). 2. **`ctr_ml_view`**: ๋ณต์žกํ•œ ์ง‘๊ณ„ ์ฟผ๋ฆฌ๋ฅผ ๋Œ€์ฒด. 1๋ถ„ ๋‹จ์œ„๋กœ ๋ฏธ๋ฆฌ ํ•ฉ๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ด ๋‘  (`AggregatingMergeTree`). @@ -70,13 +70,13 @@ ClickHouse์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์ธ `Materialized View`๋ฅผ ํ™œ์šฉํ•˜๋ฉด, ๋ณ„๋„์˜ ## 4. ๊ฐ€์„ค ๊ฒ€์ฆ -์‹ค์ œ๋กœ Redis์™€ Serving API ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ClickHouse๋กœ ํ†ตํ•ฉํ•ด ๋ณธ ๊ฒฐ๊ณผ๋Š” ์•„์ฃผ ๋งค์šฐ ๋งŒ์กฑ์Šค๋Ÿฌ์› ์Šต๋‹ˆ๋‹ค. +์‹ค์ œ๋กœ Redis์™€ Serving API ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ œ๊ฑฐํ•˜๊ณ  ClickHouse๋กœ ํ†ตํ•ฉํ•˜์ž ๋กœ์ปฌ ์‹คํ–‰ ์•ˆ์ •์„ฑ๊ณผ ์ฒ˜๋ฆฌ๋Ÿ‰์ด ํ•จ๊ป˜ ๊ฐœ์„ ๋๋‹ค. ![](/images/posts/๋งฅ๋ถ์—์–ด-m1-์ƒํ™œ/b0ef8cfa7bfd.png) -### **๐Ÿš€ ์„ฑ๋Šฅ ์ฆ๋ช… (๋กœ์ปฌ ๋ฒค์น˜๋งˆํฌ)** +### ์„ฑ๋Šฅ ๊ฒ€์ฆ: ๋กœ์ปฌ ๋ฒค์น˜๋งˆํฌ -์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ…Œ์ด๋ธ” ์บ์‹œ๋ฅผ ์˜ˆ์—ดํ•œ ๋’ค 10ํšŒ ์กฐํšŒํ•˜์—ฌ ํ‰๊ท ์น˜๋ฅผ ๊ณ„์‚ฐํ–ˆ์Šต๋‹ˆ๋‹ค. (TABiX ์‚ฌ์šฉ) +์„ฑ๋Šฅ์„ ์ธก์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ…Œ์ด๋ธ” ์บ์‹œ๋ฅผ ์˜ˆ์—ดํ•œ ๋’ค 10ํšŒ ์กฐํšŒํ•˜์—ฌ ํ‰๊ท ์น˜๋ฅผ ๊ณ„์‚ฐํ–ˆ๋‹ค. ์ธก์ • ๋„๊ตฌ๋Š” TABiX๋ฅผ ์‚ฌ์šฉํ–ˆ๋‹ค. ```sql ex. SELECT count() FROM ctr_results_raw; @@ -85,15 +85,15 @@ ex. SELECT count() FROM ctr_results_raw; - **๋‹จ๊ฑด ์กฐํšŒ**: 0.00 ~ 0.01s (Redis์˜ 1ms๋ณด๋‹ค๋Š” ๋А๋ฆฌ์ง€๋งŒ, ๋Œ€์‹œ๋ณด๋“œ์šฉ์œผ๋กœ๋Š” ์ถฉ๋ถ„ํžˆ ๋น ๋ฆ„) - **์ง‘๊ณ„ ์กฐํšŒ**: 0.02 ~ 0.05s (๊ธฐ์กด Python API์—์„œ ์ง์ ‘ ์ง‘๊ณ„ํ•  ๋•Œ๋ณด๋‹ค ์˜คํžˆ๋ ค 10๋ฐฐ ์ด์ƒ ๋น ๋ฆ„) -์ •ํ™•ํ•˜๊ฒŒ ms ๋‹จ์œ„๊นŒ์ง€ ํ™•์ธํ•˜๊ณ  ์‹ถ์—ˆ์ง€๋งŒ ํˆด์˜ ํ•œ๊ณ„๋กœ 10ms ๋‹จ์œ„๊นŒ์ง€๋งŒ ํ™•์ธ ๊ฐ€๋Šฅํ–ˆ์Šต๋‹ˆ๋‹ค.. +์ •ํ™•ํ•œ ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„๊นŒ์ง€ ํ™•์ธํ•˜๊ณ  ์‹ถ์—ˆ์ง€๋งŒ, ๋„๊ตฌ์˜ ํ•œ๊ณ„๋กœ 10ms ๋‹จ์œ„๊นŒ์ง€๋งŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. -### **๐Ÿงฑ ์•„ํ‚คํ…์ฒ˜ ๋‹จ์ˆœํ™” ํšจ๊ณผ** +### ์•„ํ‚คํ…์ฒ˜ ๋‹จ์ˆœํ™” ํšจ๊ณผ ![](/images/posts/๋งฅ๋ถ์—์–ด-m1-์ƒํ™œ/646e42689b31.png) -- Resource : Redis์™€ API ์„œ๋ฒ„๊ฐ€ ๋จน๋˜ ์•ฝ 1GB์˜ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํ™•๋ณดํ•ด Flink์— ๋” ํ• ๋‹นํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. -- SQL ์ค‘์‹ฌ : ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ SQL ํ•˜๋‚˜๋กœ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด ๋””๋ฒ„๊น…์ด ํ›จ์”ฌ ์‰ฌ์›Œ์กŒ์Šต๋‹ˆ๋‹ค. -- Deployment: `docker-compose.yml` ์™€ ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€์— ์ฝ”๋“œ๊ฐ€ ์ค„์–ด๋“ค์–ด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์šฉ์ดํ•ด์กŒ์Šต๋‹ˆ๋‹ค. +- Resource: Redis์™€ API ์„œ๋ฒ„๊ฐ€ ์‚ฌ์šฉํ•˜๋˜ ์•ฝ 1GB์˜ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํ™•๋ณดํ•ด Flink์— ๋” ํ• ๋‹นํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. +- SQL ์ค‘์‹ฌ: ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ SQL ํ•˜๋‚˜๋กœ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์–ด ๋””๋ฒ„๊น… ๊ฒฝ๋กœ๊ฐ€ ๋‹จ์ˆœํ•ด์กŒ๋‹ค. +- Deployment: `docker-compose.yml`๊ณผ ํ”„๋กœ์ ํŠธ ๋‚ด๋ถ€ ์ฝ”๋“œ๊ฐ€ ์ค„์–ด๋“ค์–ด ์œ ์ง€๋ณด์ˆ˜๊ฐ€ ์‰ฌ์›Œ์กŒ๋‹ค. --- @@ -108,14 +108,14 @@ ex. SELECT count() FROM ctr_results_raw; | ์ง€์—ฐ ์‹œ๊ฐ„ | 1~3ms | 10~30ms | | ์ฒ˜๋ฆฌ๋Ÿ‰ | ~4k | ํ‰๊ท  20k, ์ŠคํŒŒ์ดํฌ 55k | -## 6. ๋А๋‚€์  +## 6. ๋‚จ์€ ๊ธฐ์ค€ -์‚ฌ์‹ค ์ฒ˜์Œ์—๋Š” ๋‚ด ๋งฅ๋ถ์˜ ์‚ฌ์–‘์ด ๋‚ฎ์•„์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์ดˆ๋‹น 833๊ฑด์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š”๋ฐ๋„ ๋ฒ„๋ฒ…์ด๋ฉฐ ํŒฌ์ด ์—„์ฒญ๋‚œ ๊ต‰์Œ์„ ๋ƒˆ๊ธฐ ๋•Œ๋ฌธ์ด์ฃ . +์ฒ˜์Œ์—๋Š” MacBook Air M1 ยท 8GB RAM์ด๋ผ๋Š” ํ™˜๊ฒฝ ๋•Œ๋ฌธ์— ์ƒ๊ธด ๋ฌธ์ œ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค. ์ดˆ๋‹น 833๊ฑด์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋„ ๋ฐœ์—ด๊ณผ ๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ•์ด ์ปธ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. -ํ•˜์ง€๋งŒ ๋” ๋‚˜์€ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•ด ๊ณ„์† ๊ณ ๋ฏผํ•˜๋ฉฐ ์ตœ์ ํ™”๋ฅผ ํ•˜๋‹ค ๋ณด๋‹ˆ ์ฒ˜๋ฆฌ๋Ÿ‰์€ ์ดˆ๋‹น 4์ฒœ ๊ฑด๊นŒ์ง€ ๋Š˜์–ด๋‚ฌ๊ณ , Redis + Serving API๋ฅผ ์ œ๊ฑฐํ•ด ๋ฆฌ์†Œ์Šค๋ฅผ ํ™•๋ณดํ•œ ๋’ค Flink ์•ฑ์— ์ž์›์„ ๋” ํ• ๋‹นํ•˜์ž ํ‰๊ท  1~2๋งŒ ๊ฑด์„ ์ฒ˜๋ฆฌํ•˜๋ฉฐ ์•ฝ 5.5๋งŒ ๊ฑด์˜ ์ŠคํŒŒ์ดํฌ ํŠธ๋ž˜ํ”ฝ๋„ ์•ˆ์ •์ ์œผ๋กœ ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. +ํ•˜์ง€๋งŒ ๊ตฌ์กฐ๋ฅผ ๋‹ค์‹œ ์‚ดํŽด๋ณด๋ฉฐ ์ตœ์ ํ™”๋ฅผ ์ง„ํ–‰ํ•˜์ž ์ฒ˜๋ฆฌ๋Ÿ‰์€ ์ดˆ๋‹น 4์ฒœ ๊ฑด๊นŒ์ง€ ๋Š˜์–ด๋‚ฌ๋‹ค. ์ดํ›„ Redis + Serving API๋ฅผ ์ œ๊ฑฐํ•ด ๋ฆฌ์†Œ์Šค๋ฅผ ํ™•๋ณดํ•˜๊ณ  Flink ์•ฑ์— ์ž์›์„ ๋” ํ• ๋‹นํ•˜์ž ํ‰๊ท  1~2๋งŒ ๊ฑด์„ ์ฒ˜๋ฆฌํ–ˆ๊ณ , ์•ฝ 5.5๋งŒ ๊ฑด์˜ ์ŠคํŒŒ์ดํฌ ํŠธ๋ž˜ํ”ฝ๋„ ์•ˆ์ •์ ์œผ๋กœ ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. ![](/images/posts/๋งฅ๋ถ์—์–ด-m1-์ƒํ™œ/9b51c74eac5a.png) -๊ณผ์—ฐ ์ตœ์‹  ๋ฒ„์ „์˜ ๋งฅ๋ถ์„ ์‚ฌ์šฉํ•ด ์ฒ˜์Œ๋ถ€ํ„ฐ ์ž์›์ด ๋„‰๋„‰ํ–ˆ๋‹ค๋ฉด, ์ด๋ ‡๊ฒŒ๊นŒ์ง€ ์ตœ์ ํ™”๋ฅผ ์‹œ๋„ํ–ˆ์„๊นŒ? ํ•˜๋Š” ์ƒ๊ฐ๋„ ๋“ญ๋‹ˆ๋‹ค. ์˜คํžˆ๋ ค ์ด๋Ÿฐ ์ œํ•œ๋œ ํ™˜๊ฒฝ ๋•๋ถ„์— Flink๋ฅผ ๋” ๊นŠ์ด ์‚ดํŽด๋ณด๊ณ , ์ตœ์ ํ™”๋ฅผ ์œ„ํ•œ ์—ฌ์ •์„ ๊ฒฝํ—˜ํ•  ์ˆ˜ ์žˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. +์ฒ˜์Œ๋ถ€ํ„ฐ ์ž์›์ด ๋„‰๋„‰ํ–ˆ๋‹ค๋ฉด ์ด ์ •๋„๋กœ ๊ตฌ์กฐ๋ฅผ ์˜์‹ฌํ•˜์ง€ ์•Š์•˜์„ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’๋‹ค. ์ œํ•œ๋œ ๋กœ์ปฌ ํ™˜๊ฒฝ์€ ๋‹จ์ˆœํ•œ ๋ถˆํŽธํ•จ์ด ์•„๋‹ˆ๋ผ, Flink ๋ณ‘๋ ฌ๋„์™€ ์ƒํƒœ ๊ด€๋ฆฌ, ์ €์žฅ์†Œ ์„ ํƒ ๊ธฐ์ค€์„ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ๊ฒ€์ฆํ•˜๊ฒŒ ๋งŒ๋“  ์ œ์•ฝ์ด์—ˆ๋‹ค. -์•„์ง ์ด ์‹œ์Šคํ…œ์ด ์™„๋ฒฝํ•˜๋‹ค๊ณ ๋Š” ์ƒ๊ฐํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์ตœ์†Œ 10๋งŒ ๊ฑด์˜ ์ŠคํŒŒ์ดํฌ ํŠธ๋ž˜ํ”ฝ์„ ๊ฒฌ๋”œ ์ˆ˜ ์žˆ๋„๋ก ๋” ์ตœ์ ํ™”ํ•˜๋ฉฐ, ์ง€์†์ ์œผ๋กœ ์„ฑ์žฅ์‹œ์ผœ ๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค. +์•„์ง ์ด ์‹œ์Šคํ…œ์ด ์™„์„ฑ๋๋‹ค๊ณ  ๋ณด์ง€๋Š” ์•Š๋Š”๋‹ค. ๋‹ค์Œ ๊ธฐ์ค€์€ ์ตœ์†Œ 10๋งŒ ๊ฑด์˜ ์ŠคํŒŒ์ดํฌ ํŠธ๋ž˜ํ”ฝ์„ ๊ฒฌ๋”œ ์ˆ˜ ์žˆ๋„๋ก ๋ณ‘๋ชฉ์„ ๋” ์ขํžˆ๊ณ , ์„ฑ๋Šฅ ๊ฐœ์„ ์ด ์–ด๋–ค ๊ตฌ์กฐ์  ์„ ํƒ์—์„œ ๋‚˜์™”๋Š”์ง€ ๊ณ„์† ์„ค๋ช… ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด๋‹ค. diff --git "a/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/meta.json" "b/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/meta.json" index 8dd709f..b07a827 100644 --- "a/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/meta.json" +++ "b/posts/\353\247\245\353\266\201\354\227\220\354\226\264-m1-\354\203\235\355\231\234/meta.json" @@ -1,7 +1,7 @@ { - "title": "๋กœ์ปฌ CTR ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์กฐ ๋‹จ์ˆœํ™”์™€ ์„ฑ๋Šฅ ๊ฐœ์„ ", + "title": "์‹ค์‹œ๊ฐ„ CTR ํŒŒ์ดํ”„๋ผ์ธ ์„ฑ๋Šฅ๊ฐœ์„ ๊ธฐ", "slug": "macbook-air-m1-life", - "description": "๋กœ์ปฌ CTR ํŒŒ์ดํ”„๋ผ์ธ์—์„œ Redis์™€ API๋ฅผ ๋œ์–ด๋‚ด๊ณ  ClickHouse ์ค‘์‹ฌ์œผ๋กœ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•œ ๊ณผ์ •์„ ์ •๋ฆฌํ•œ๋‹ค.", + "description": "์‹ค์‹œ๊ฐ„ CTR ํŒŒ์ดํ”„๋ผ์ธ์—์„œ Redis์™€ API๋ฅผ ๋œ์–ด๋‚ด๊ณ  ClickHouse ์ค‘์‹ฌ์œผ๋กœ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•œ ๊ณผ์ •์„ ์ •๋ฆฌํ•œ๋‹ค.", "date": "2025-01-20", "category": "Tech", "tags": [ diff --git "a/posts/\354\213\244\354\213\234\352\260\204-CTR-\355\214\214\354\235\264\355\224\204\353\235\274\354\235\270-\352\265\254\354\266\225\352\270\260/meta.json" "b/posts/\354\213\244\354\213\234\352\260\204-CTR-\355\214\214\354\235\264\355\224\204\353\235\274\354\235\270-\352\265\254\354\266\225\352\270\260/meta.json" index 5627f2d..2880824 100644 --- "a/posts/\354\213\244\354\213\234\352\260\204-CTR-\355\214\214\354\235\264\355\224\204\353\235\274\354\235\270-\352\265\254\354\266\225\352\270\260/meta.json" +++ "b/posts/\354\213\244\354\213\234\352\260\204-CTR-\355\214\214\354\235\264\355\224\204\353\235\274\354\235\270-\352\265\254\354\266\225\352\270\260/meta.json" @@ -1,7 +1,7 @@ { - "title": "์‹ค์‹œ๊ฐ„ CTR ์ง‘๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ• ๊ธฐ๋ก", + "title": "์‹ค์‹œ๊ฐ„ CTR ์ง‘๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•๊ธฐ", "slug": "ctr-pipeline", - "description": "CTR ์ง‘๊ณ„๋ฅผ ์šด์˜ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์œผ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์›Œํ„ฐ๋งˆํฌ, ์œˆ๋„์šฐ, ์ง€์—ฐ ํ—ˆ์šฉ, ๊ฒ€์ฆ ์ „๋žต์„ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ–ˆ๋Š”์ง€ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.", + "description": "CTR ์ง‘๊ณ„๋ฅผ ์šด์˜ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์œผ๋กœ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ์›Œํ„ฐ๋งˆํฌ, ์œˆ๋„์šฐ, ์ง€์—ฐ ํ—ˆ์šฉ, ๊ฒ€์ฆ ์ „๋žต์„ ์–ด๋–ป๊ฒŒ ์„ค๊ณ„ํ–ˆ๋Š”์ง€ ์ •๋ฆฌํ•œ๋‹ค.", "date": "2025-01-20", "category": "Tech", "tags": [