923 posts
Rust에서 구조체 없이 JSON 다루기: serde_json

Rust에서 구조체 없이 JSON 다루기: serde_json

Serde로 데이터를 직렬화할 때는 보통 구조체를 먼저 선언하고 #[derive(Serialize, Deserialize)]를 붙이는데요. 데이터의 모양을 미리 알고 있다면 이 방식이 가장 안전하고 깔끔합니다. 그런데 현업에서는 구조체로 미리 못 박기 애매한 경우도 자주 만나게 됩니다. 외부 API가 내려주는 응답 중 일부 필드만 필요하거나, 응답 구조가 자주 바뀌거나, 요청 본문을 그때그때 즉석에서 조립해야 할 때가 그렇죠. 이럴 때 구조체를 일일이 정의하는 건 오히려 번거롭습니다. 그래서 이 글에서는 serde_json이 제공하

실시간 단방향 통신을 위한 Server-Sent Events(SSE)

실시간 단방향 통신을 위한 Server-Sent Events(SSE)

ChatGPT에게 질문을 던지면 답변이 한 글자씩 타이핑되듯 흘러나오는 모습, 다들 익숙하시죠? 증권 앱의 시세가 새로고침 없이 실시간으로 바뀌거나, 웹 대시보드에 알림이 띵 하고 도착하는 것도 마찬가지인데요. 이런 화면들의 공통점은 서버에서 새로운 데이터가 생길 때마다 클라이언트가 곧바로 받아본다는 것입니다. 실시간 양방향 통신을 위한 웹소켓(WebSocket) 글에서 우리는 HTTP의 한계와 이를 극복하는 웹소켓을 살펴봤는데요. 그런데 위 예시를 가만히 보면 한 가지 공통점이 더 있습니다. 데이터가 서버에서 클라이언트로 한 방향

Rust mod 키워드: 모듈을 선언하고 파일을 나누는 법

Rust mod 키워드: 모듈을 선언하고 파일을 나누는 법

Rust를 처음 배우면서 파일을 나누기 시작하면 mod에서 한 번쯤 멈칫하게 됩니다. 다른 언어의 import나 require에 익숙하다면 mod user;를 보고 "아, user 파일을 가져오는 건가?"라고 생각하기 쉬운데요. 반은 맞고 반은 틀립니다. mod는 파일을 읽어 오는 명령이라기보다 이 위치에 이런 이름의 모듈이 있다고 선언하는 키워드입니다. 그 모듈의 내용이 같은 파일 안에 있을 수도 있고, 별도 파일에 있을 수도 있죠. 이번 글에서는 Rust의 mod 키워드가 어떤 일을 하는지, main.rs와 lib.rs에서 모듈

하이브리드 검색과 RRF: 키워드 검색과 의미 검색을 한 줄로 합치기

하이브리드 검색과 RRF: 키워드 검색과 의미 검색을 한 줄로 합치기

지난 두 편에서 검색의 두 갈래를 따로 살펴봤는데요. BM25 키워드 검색은 단어가 정확히 일치할 때 강하고, 의미 검색은 표현이 달라도 뜻으로 문서를 찾아줍니다. 그리고 두 글 모두 마지막에 "둘을 합치면 어떨까?"라는 떡밥을 남겨뒀죠. 키워드 검색의 정확함과 의미 검색의 유연함을 동시에 누리자는 게 하이브리드 검색(hybrid search)의 아이디어입니다. 말은 간단한데, 막상 합치려고 하면 곧바로 벽에 부딪힙니다. 두 검색기가 내놓는 점수가 완전히 다른 단위거든요. BM25는 0에서 수십까지 제한 없이 커지는 점수를 주고,

Discord MCP 서버로 AI에게 커뮤니티 운영 맡기기

Discord MCP 서버로 AI에게 커뮤니티 운영 맡기기

개발 커뮤니티나 팀의 소통 채널로 Discord를 운영하고 있으면 은근히 손이 가는 일이 많은데요. 새 버전을 릴리스하면 공지 채널에 변경 사항을 정리해서 올리고, 질문 채널에 쌓인 메시지를 읽고 답하고, 행사가 잡히면 이벤트를 등록합니다. 하나하나는 금방 끝나는 일이지만 매번 Discord 창을 열고 손으로 처리하다 보면 흐름이 자꾸 끊기죠. 😅 만약 코딩 중인 AI 도구에 "방금 머지된 PR 내용을 정리해서 #release 채널에 공지해줘"라고 말하면 그대로 처리해주고, "어제 질문 채널에 올라온 메시지 중에 아직 답이 없는

의미 검색(Semantic Search)은 어떻게 동작할까: 임베딩과 벡터 유사도

의미 검색(Semantic Search)은 어떻게 동작할까: 임베딩과 벡터 유사도

전에 BM25 랭킹 알고리즘을 다루면서 마지막에 숙제를 하나 남겨뒀는데요. "환불"로 검색하면 정작 "결제 취소"가 적힌 안내 문서를 찾지 못한다는 키워드 검색의 약점이었습니다. 두 표현은 사람이 보기엔 같은 뜻이지만, 글자가 하나도 겹치지 않으니 단어 일치에 기대는 검색은 둘을 연결하지 못해요. 😅 이 빈틈을 메우는 게 바로 의미 검색(Semantic Search)입니다. 글자가 아니라 "의미"를 기준으로 문서를 찾는 방식이죠. 요즘 챗봇, 추천, RAG(검색 증강 생성)의 바탕에 거의 빠짐없이 깔려 있는 기술이기도 합니다. 이

BM25 알고리즘: 검색 엔진은 어떻게 관련도 순으로 정렬할까

BM25 알고리즘: 검색 엔진은 어떻게 관련도 순으로 정렬할까

검색창에 단어를 넣으면 결과가 주르륵 나오는데요. 그런데 이 결과들의 "순서"는 누가 정하는 걸까요? 단순히 단어가 들어간 문서를 다 모아서 보여주기만 한다면, 정작 내가 찾던 문서는 47번째쯤에 묻혀 있을 수도 있어요. 검색이 쓸모 있으려면 가장 관련 있는 문서가 맨 위로 올라와야 합니다. 이 "관련도 순 정렬"을 책임지는 게 바로 랭킹 함수입니다. 그리고 지난 20여 년간 텍스트 검색의 사실상 표준으로 자리 잡은 랭킹 함수가 바로 BM25예요. Elasticsearch, OpenSearch, Apache Lucene, SQLit

Rust 모듈 시스템 큰 그림: mod, use, pub이 맞물리는 방식

Rust 모듈 시스템 큰 그림: mod, use, pub이 맞물리는 방식

Rust를 배우다 보면 어느 순간 코드가 한 파일에 다 들어가지 않게 됩니다. 처음에는 main.rs 하나로 충분하지만, 타입이 늘고 함수가 많아지면 자연스럽게 파일을 나누고 싶어지죠. 그때 등장하는 키워드가 mod, use, pub입니다. 문제는 이 셋이 비슷한 위치에 자주 나타난다는 점입니다. 파일 위쪽에 mod config;가 있고, 바로 아래에 use crate::config::Config;가 있으며, 다른 파일에는 pub struct Config가 있죠. 처음 보면 "이게 다 import인가?", "왜 pub을 붙였는데 밖

Void로 프로덕션 앱 이전하기: 하루 만의 컷오버 실전 기록

Void로 프로덕션 앱 이전하기: 하루 만의 컷오버 실전 기록

지난번 Void는 Vite 네이티브 배포 플랫폼이라는 글에서 "이게 자바스크립트의 Rails가 될 수 있을까"를 다뤘는데요. 그때는 직접 써보지 않고 개념만 파헤친 소개글이었습니다. 이번엔 다릅니다. TanStack Start + Cloudflare Workers + D1으로 운영 중이던 실서비스를 하루 만에 Void로 컷오버하면서 직접 부딪힌 기록이에요. 결론부터 말씀드리면 코드 변경은 거의 없었고, 빌드는 3배 빨라졌고, 그리고 베타 제품답게 문서에 없는 지뢰를 다섯 개나 밟았습니다. 💥 이 글은 그 지뢰들을 어떻게 역공학으로

Rust에서 큐(Queue) 자료구조 사용하기: VecDeque

Rust에서 큐(Queue) 자료구조 사용하기: VecDeque

큐(queue)는 먼저 들어온 데이터가 먼저 나가는 선입선출(First In First Out, 이하 FIFO) 자료구조입니다. 줄을 서서 기다리는 모습을 떠올리면 됩니다. 먼저 줄을 선 사람이 먼저 처리되죠. 작업 대기열, 메시지 버퍼, 너비 우선 탐색(BFS) 같은 곳에서 빠지지 않고 등장합니다. 그런데 Rust를 막 시작한 분들은 큐가 필요할 때 일단 Vec에 손이 갑니다. Vec의 끝에 push로 넣고, 앞에서 remove(0)으로 빼면 되니까요. 동작은 하는데요, 문제가 하나 있습니다. Vec은 메모리에 데이터를 일렬로 붙

Void 환경변수: env.ts로 타입 안전하게, 시크릿까지

Void 환경변수: env.ts로 타입 안전하게, 시크릿까지

Void 인증을 붙이고 나면 곧바로 BETTER_AUTH_SECRET이라는 비밀 키를 마주하게 됩니다. 세션 토큰을 서명하는 값인데, 코드에 적을 수도 없고 깃에 올릴 수도 없죠. 비단 인증만의 문제도 아닙니다. Stripe 키, 웹훅 URL, 외부 API 토큰까지, 앱을 키우다 보면 "코드 밖에서 주입해야 하는 값"이 계속 늘어나거든요. 그런데 우리가 환경변수로 늘 겪던 불편이 있습니다. process.env.PORT는 항상 문자열이라 매번 숫자로 바꿔야 하고, 키 이름에 오타가 나도 런타임에야 터지고, 어떤 값이 필수인지는 RE

Cloudflare Access 신원 공급자(IdP) 연동 제대로 이해하기

Cloudflare Access 신원 공급자(IdP) 연동 제대로 이해하기

Cloudflare Access로 내부 대시보드를 막아두고 나면, 자연스럽게 다음 질문이 따라옵니다. "그래서 로그인은 대체 누가 처리하는 거지?" Access 설정 화면 어디에도 비밀번호를 저장하는 곳은 없고, 사용자 목록을 직접 만드는 메뉴도 보이지 않거든요. 그런데도 @mycompany.com 직원만 통과시키고, 인턴 그룹은 제외하고, MFA를 강제하는 정책이 멀쩡히 돌아갑니다 🤔 비밀은 Access가 인증을 직접 하지 않는다는 데 있습니다. 대신 외부 신원 공급자(Identity Provider, 이하 IdP)에게 "이 사

auth.md: AI 에이전트를 위한 회원가입 프로토콜

auth.md: AI 에이전트를 위한 회원가입 프로토콜

요즘 AI 에이전트가 단순히 질문에 답변만 하는 게 아니라, 실제로 사용자를 대신해서 외부 서비스를 호출하는 경우가 점점 늘고 있는데요. 예를 들어 코딩 에이전트한테 "Cloudflare에 새 워커를 배포해줘"라고 시키거나, 이메일 에이전트한테 "Resend로 뉴스레터 발송해줘"라고 부탁할 수 있습니다. 그런데 여기서 한 가지 문제가 생깁니다. 에이전트가 그 서비스에 가입되어 있어야 API를 호출할 수 있는데, 가입 자체가 사람을 전제로 만들어진 절차란 말이죠. 회원가입 폼, 이메일 인증, CAPTCHA, 대시보드 로그인 같은 단계

선배, 저 개발하지 말라고 해줘서 너무 고마워요

선배, 저 개발하지 말라고 해줘서 너무 고마워요

"선배, 저 개발하지 말라고 해줘서 너무 고마워요!" 얼마 전, 한국에서 함께 일하던 후배에게 연락이 왔습니다. 임원으로 승진했다는 반가운 소식이었습니다. 그 친구가 신입이었을 때 제가 사수를 맡았는데, 개발에는 소질이 없는 것 같다고 직언을 해준 적이 있습니다. 요즘이야 멘토링도 하고 그래서 조금 나아졌지만, 당시에 저는 돌려서 말하는 방법을 잘 몰랐습니다. 저의 묵직한 돌직구를 후배는 아직도 기억하고 있더라고요. 안 그래도 본인은 개발이 적성에 맞지 않는 것 같다고 느끼고 있었는데, 제 말이 계기가 되어 개발자로서의 커리어에 대

Satteri: Rust로 파싱하고 JavaScript로 확장하는 마크다운 처리기

Satteri: Rust로 파싱하고 JavaScript로 확장하는 마크다운 처리기

블로그나 문서 사이트를 운영하다 보면 빌드 시간이 슬금슬금 늘어나는 순간이 옵니다. 글이 수백 개를 넘어가면 마크다운 파싱과 변환에만 수십 초가 쓰이고, 거기에 remark/rehype 플러그인을 몇 개 더 끼우면 1분을 훌쩍 넘기는 경우도 흔하죠. 저도 이 블로그를 운영하면서 빌드 로그를 보다가 "마크다운 처리에 이만큼 시간을 쓴다고?"라는 생각을 한두 번 해본 게 아닌데요 😅 최근 Astro 6.4에서 markdown.processor API가 새로 등장하면서, unified 생태계가 아닌 다른 마크다운 처리기로 갈아탈 수 있

Void 인증: Better Auth로 로그인부터 라우트 보호까지

Void 인증: Better Auth로 로그인부터 라우트 보호까지

Void 데이터베이스에서 테이블을 만들고 Void 라우팅으로 API까지 열고 나면, 다음으로 부딪히는 벽은 늘 똑같습니다. "그래서 이 요청을 보낸 사람이 누구인지 어떻게 알지?"라는 질문이죠. 로그인 없는 앱은 결국 누구나 같은 화면을 보는 앱이니까요. 다행히 Void는 인증을 거의 공짜로 얹어줍니다. 따로 라이브러리를 고르고 세션 저장소를 붙이고 콜백을 짜는 그 지난한 과정 없이, 설정 한 줄과 헬퍼 몇 개로 로그인이 돌아가거든요. 이번 글에서는 인증을 켜고, 회원가입과 로그인을 붙이고, "로그인한 사람만 들어올 수 있는" 영역

tantivy로 시작하는 Rust 풀텍스트 검색: 스키마부터 쿼리까지

tantivy로 시작하는 Rust 풀텍스트 검색: 스키마부터 쿼리까지

데이터베이스에 LIKE '%검색어%'를 박아 쓰다 보면 한계가 금방 옵니다. 결과는 느리고, 오타에 약하고, 관련도 점수도 없습니다. 본격적으로 검색다운 검색을 붙이려면 Elasticsearch나 OpenSearch를 띄우는 게 정석이지만, 가벼운 사이드 프로젝트나 임베디드 환경에서 별도 서버까지 두기는 부담스러울 때가 있어요. tantivy는 Rust로 작성된 풀텍스트 검색 엔진 라이브러리입니다. Apache Lucene에서 영감을 받은 설계라서 한 번 배워두면 검색 엔진의 기본기를 그대로 익힐 수 있고, 별도의 데몬 없이 우리

MCP 2026-07-28 스펙: 세션을 버리고 무상태 프로토콜로

MCP 2026-07-28 스펙: 세션을 버리고 무상태 프로토콜로

MCP 서버를 직접 만들어보신 분이라면 한 번쯤 이런 고민을 해보셨을 것 같은데요. 서버를 여러 대로 늘려서 트래픽을 나눠 받으려는데, 클라이언트마다 세션이 묶여 있어서 같은 서버로만 요청을 보내야 하는 상황 말이죠. 로드밸런서에 스티키 세션(sticky session)을 걸거나 세션 정보를 따로 저장소에 빼두거나, 방법을 찾긴 하지만 어딘가 번거롭고 찜찜했습니다. 😅 지난 7월 28일에 최종 확정된 MCP 2026-07-28 스펙이 바로 이 지점을 정면으로 건드립니다. 프로토콜이 세상에 나온 이후 가장 큰 폭의 개정으로, 공동

Void 폼과 액션: useForm으로 끝까지 타입 안전하게

Void 폼과 액션: useForm으로 끝까지 타입 안전하게

Void 라우팅에서 데이터를 읽어 오는 로더는 자세히 봤는데요. 정작 데이터를 바꾸는 쪽은 "로더 대신 action을 내보낸다"는 한 줄로 넘어갔습니다. 사실 폼을 다루는 일은 읽기보다 손이 많이 가죠. 입력값을 검증하고, 에러를 화면에 표시하고, 제출 중에는 버튼을 막고, 성공하면 목록을 새로고침하고… 이번 글에서는 Void가 이 과정을 useForm 하나로 어떻게 묶어주는지 살펴보겠습니다. 액션으로 데이터 바꾸기 데이터를 읽을 때 로더를 썼다면, 바꿀 때는 액션을 씁니다. 페이지의 .server.ts에서 action을 내보내면

bat 사용법: cat을 대체하는 문법 강조 지원 뷰어

bat 사용법: cat을 대체하는 문법 강조 지원 뷰어

터미널에서 코드 파일을 빠르게 훑어보고 싶을 때 보통 cat file.py라고 입력하는데요. 출력은 잘 되지만 색도 없고 줄 번호도 없어서 조금만 길어져도 가독성이 뚝 떨어집니다. 그렇다고 매번 에디터를 켜기엔 번거롭고, less로 열면 페이징은 되지만 여전히 흑백이라 아쉽죠 😅 이런 답답함을 해결해 주는 도구가 바로 bat입니다. 이름에서 짐작하셨겠지만 cat을 본떠 만든 도구인데요. Rust로 작성됐고 ripgrep, fd처럼 "기존 유닉스 도구를 현대적으로 다시 만든" 계열에 속합니다. 문법 강조, Git 통합, 줄 번호,

Discord