염산하
@[email protected] · Reply to 염산하's post
@ysh 이제 정신 차려야지.
@[email protected] · 23 following · 15 followers
A software engineer in Seoul, and a father of a kid.
@[email protected] · Reply to 염산하's post
@ysh 이제 정신 차려야지.
페북에서 파토님이 절대회귀라는 웹소설이 완결되었는데 아무튼 강추라고해서 시작했다가... 10일만에 다 읽었다. 도저히 적당히 읽을 수가 없어서 거의 모든 시간을 읽는데 쏟았네. 이게 뭐라고 보는내내 울고웃고 너무 몰입했다. 네이버 시리즈. 총 896회. 22년 10월부터 매일 연재한 작품이었네. 장영훈 작가.
원래 옵시디언으로 노트를 많이 만들어놨는데 카르파시 선생의 LLM wiki 내용으로 스킬 만들어서 폴더 하나 정해주고 정리시키니까 자미있네
정확치도 않은 사실을 포함한채로 AI 생성 문서를 위키에 올려놓으면 ... 쓰레기가 남으면 영원히 삽질의 근원이 될 것 같아서 고쳐야 하는데 기분은 별로고...
https://hollo.woojinkim.org/@me/019cf945-06b1-7dc4-8274-d63ed71d33f4
세심하게 작성되지는 않았지만 어쨌든 필요한 내용을 모두 포함한 문서에 대해 이전같으면 더 잘 쓰는 방법을 피드백 하기도 했지만 ...... 이젠 그냥 기계한테 읽고 정리하라고 하기 때문에 문서를 더 잘 쓰라고 피드백하지 않게 됐다.
@[email protected] · Reply to Woojin Kim's post
@me 엄청 많은 드론이 필요해지는 거 아닐까요?
@[email protected] · Reply to Woojin Kim's post
@me 오 진짜요?
밖에서 맥북 에어나 아이패드같은 가벼운 기기에서 SSH를 통해 원격 작업을 해보고 싶어져서 테스트를 해보면서 연결이 끊기면 작업하던 내용이 멈추거나 하는 것들이 불편해 세션 유지 도구가 필요하겠다는 생각이 들었습니다. 하지만 저는 예전부터 tmux를 써보려고 여러 차례 시도했지만 어떻게 해도 손에 익지 않아 금방 포기하게 되었는데, 옆에서 이야기를 들으셨던 @hongminhee 님에게 추천을 받아서 Zellij를 알게 되었고 시도해보게 되었습니다.
Zellij를 사용해보니 기대했던 것 이상으로 사용 경험이 좋고 커스터마이징 등 설정도 어렵지 않아서 매우 만족하고 있습니다. 그래서 오늘은 Zellij를 소개하고 저의 경험과 설정도 공유해보려고 합니다.
Zellij은 Rust로 작성된 터미널 멀티플렉서입니다. tmux와 같은 범주의 도구로 터미널 안에서 탭을 만들거나 화면을 분할하고 세션을 유지할 수 있습니다.
tmux와 비교했을 때 제가 느낀 차이는 진입 장벽입니다. Zellij을 처음 실행하면 화면 하단에 현재 모드에서 쓸 수 있는 단축키가 바로 표시됩니다. 설정 파일은 KDL이라는 포맷을 쓰는데 읽거나 작성하는데 크게 어렵지 않습니다. 모드 기반 인터페이스라는 점은 tmux와 비슷하지만, 뭘 눌러야 하는지 화면에서 바로 알 수 있어서 문서를 뒤질 필요가 적습니다.
Zellij는 여러 패키지 매니저를 지원하고 Cargo를 이용해 직접 빌드하는 것도 가능합니다. Homebrew가 설치된 macOS 환경이라면 한 줄이면 됩니다.
brew install zellij
저는 터미널 에뮬레이터에서 제공하는 기능들을 적극적으로 활용하는 편입니다. Ghostty는 자체적으로 탭 관리, 화면 분할, 검색 등 많은 기능을 내장하고 있습니다. 그래서 처음엔 Zellij를 테스트해볼 때, 이런 서로 중복되는 기능을 어떻게 처리할지에 대한 고민도 있었습니다. Ghostty가 앞에서 해당 키를 처리하고 Zellij에겐 해당 이벤트가 전달되지 않겠지만, 같은 단축키로 바인딩하면 어느 쪽의 기능을 써야할지 모호해집니다.
제가 시도한 방법은 터미널 에뮬레이터는 계속 Ghostty를 쓰지만, 이러한 기능은 모두 Zellij에게 위임하는 방향을 선택했습니다. 하지만 Ghostty의 단축키 바인딩을 그대로 써서 단축키에 익숙해지는데 시간을 들이지 않고 싶었습니다. Ghostty에서 unbind 처리를 해주면 모든 단축키 입력도 Zellij까지 전달되어, Zellij에서 이를 받아서 처리하는 형태가 되었습니다. 의외로 설정은 어렵지 않아서 바로 손에 익을 수 있었습니다.
# ~/.config/ghostty/config
# Zellij에게 넘길 키 바인딩을 전부 해제
keybind = cmd+t=unbind # 새 탭
keybind = cmd+n=unbind # 새 탭
keybind = cmd+w=unbind # 닫기
keybind = cmd+d=unbind # 세로 분할
keybind = cmd+shift+d=unbind # 가로 분할
keybind = cmd+f=unbind # 검색
keybind = cmd+c=unbind # 복사
keybind = cmd+k=unbind # 화면 지우기
keybind = cmd+[=unbind # 이전 Pane
keybind = cmd+]=unbind # 다음 Pane
keybind = cmd+1=unbind # 탭 1~9 전환
keybind = cmd+digit_1=unbind
keybind = cmd+2=unbind
keybind = cmd+digit_2=unbind
# ... cmd+9, cmd+digit_9까지 동일
# macOS의 Option 키를 Alt로 다루기 위한 설정
macos-option-as-alt = true
원래부터 있던 폰트나 테마 등의 Ghostty 설정은 수정하지 않았습니다.
Ghostty에서 풀어준 키를 Zellij 쪽에서 받습니다. 여기서 제가 신경 쓴 건 하나로, 기존 Ghostty에서의 머슬 메모리를 그대로 유지하는 것입니다.
// ~/.config/zellij/config.kdl
keybinds clear-defaults=false {
normal {
// Split — Ghostty와 같은 단축키
bind "Super d" { NewPane "Right"; SwitchToMode "normal"; }
bind "Super Shift d" { NewPane "down"; SwitchToMode "normal"; }
// 탭
bind "Super t" { NewTab; }
bind "Super n" { NewTab; }
bind "Super w" { CloseFocus; }
// Pane 이동 — Ghostty와 같은 단축키
bind "Super [" { FocusPreviousPane; }
bind "Super ]" { FocusNextPane; }
// 탭 전환 — Ghostty와 같은 단축키
bind "Super 1" { GoToTab 1; SwitchToMode "normal"; }
bind "Super 2" { GoToTab 2; SwitchToMode "normal"; }
// ... Super 9까지 동일
// 검색, 복사, 화면 지우기
bind "Super f" { SwitchToMode "entersearch"; }
bind "Super c" { Copy; }
bind "Super k" { Clear; Write 10; }
}
}
Cmd + D로 세로 분할, Cmd + Shift + D로 가로 분할. Cmd + F로 검색. Cmd + T나 Cmd + N으로 새 탭. Cmd + [과 Cmd + ]으로 Pane 이동. Cmd + 1~Cmd + 9로 탭 전환. 전부 Ghostty를 쓸 때와 같습니다. 단축키를 새로 외울 필요가 없습니다.
clear-defaults=false는 Zellij의 기본 키 바인딩을 유지하면서 위에 덮어쓰겠다는 뜻입니다. 다만 일부 기본 키는 다른 도구와 충돌해서 풀어줬습니다.
// Claude Code 등에서 쓰는 Ctrl + O, Ctrl + B와 충돌 방지
unbind "Ctrl o"
unbind "Ctrl b"
unbind "Ctrl s"
unbind "Ctrl q"
추가로 Ghostty는 마우스를 통해 분할된 탭 영역의 사이즈를 바꿀 수 있지만 Zellij는 그렇지 않기 때문에, 영역(Pane)의 크기를 편하게 바꿀 수 있도록 Alt + Shift + Arrow에 바인딩했습니다.
bind "Alt Shift left" { Resize "Increase left"; }
bind "Alt Shift down" { Resize "Increase down"; }
bind "Alt Shift up" { Resize "Increase up"; }
bind "Alt Shift right" { Resize "Increase right"; }
macOS 터미널에서 Alt + Left/Right로 단어 단위 커서 이동을 쓰는 분이 많을 것입니다. Zellij의 기본 키 바인딩 중 Alt f가 이 시퀀스와 겹치는 문제가 있습니다(#3850). 그래서 Workaround로서 Alt f를 unbind하고 다시 bind해서 해결했습니다.
unbind "Alt f"
bind "Alt F" { ToggleFloatingPanes; }
// config.kdl
session_name "Main"
attach_to_session true
on_force_close "detach"
지금 제 설정은 모든 곳에서 하나의 세션을 공유하는 형태입니다. attach_to_session true로 설정하면 Zellij을 실행할 때 기존 세션이 있으면 그대로 붙습니다. 새 터미널 창을 열어도, 터미널을 껐다 켜도 작업 중이던 화면이 유지됩니다. on_force_close "detach"는 강제 종료 시에도 세션을 죽이지 않고 분리만 해줍니다.
이게 처음 Zellij를 도입한 이유이기도 합니다. 다른 곳에서 터미널을 열거나 SSH로 접속하면 같은 세션을 공유하고 이어서 작업할 수 있습니다. 연결을 끊고 자리를 비웠다가 돌아와도 그대로 잘 돌아가는 것을 확인할 수 있습니다.
Zellij에는 기본 상태 바가 있지만 좀 더 제 취향에 맞게 꾸미고 싶어서 zjstatus를 쓰고 있습니다. 별도 설치는 필요 없습니다. 레이아웃 파일에 wasm URL을 적어두면 Zellij이 처음 실행할 때 사용자에게 확인을 받은 뒤, 알아서 받아옵니다.
// ~/.config/zellij/layouts/default.kdl
layout {
default_tab_template {
pane size=2 borderless=true {
plugin location="https://github.com/dj95/zjstatus/releases/latest/download/zjstatus.wasm" {
// 좌측: 모드 + 세션 이름
format_left "{mode} #[fg=#eceff4,bold]{session}"
// 중앙: 탭 목록
format_center "{tabs}"
// 우측: Git 브랜치 + 시간
format_right "{command_git_branch} {datetime}"
// 모드별 표시
mode_normal "#[bg=#2e3440,fg=#5e81ac,bold] NORMAL "
mode_locked "#[bg=#5e81ac,fg=#2e3440,bold] LOCKED "
mode_resize "#[bg=#b48ead,fg=#2e3440,bold]⇋ RESIZE "
// 탭 표시 — 활성 탭은 bold + italic
tab_normal "#[fg=#4c566a] {index} {name} "
tab_active "#[fg=#d8dee9,bold,italic] {index} {name} "
// Git 브랜치 (10초 간격 갱신)
command_git_branch_command "git rev-parse --abbrev-ref HEAD"
command_git_branch_format "#[fg=#5e81ac,bold] {stdout}"
command_git_branch_interval "10"
// 시간
datetime "#[fg=#2e3440,bg=#d8dee9,bold] {format}"
datetime_format "%Y-%m-%d %H:%M"
datetime_timezone "Asia/Seoul"
}
}
children
// 단축키가 익숙하지 않다면 하단에 단축키 가이드용 기본 상태 바를 추가할 수 있습니다.
pane size=1 borderless=true {
plugin location="zellij:status-bar";
}
}
}
상단 바에는 현재 모드, 세션 이름, 탭 목록, Git 브랜치, 시간이 표시됩니다. 현재 어떤 모드에 있는지, 어떤 브랜치에서 작업 중인지 한눈에 볼 수 있어서 좋습니다. 색상은 Nord 컬러 팔레트를 기준으로 지정했습니다.
지금 제 설정에서는 하단 Zellij 기본 상태 바를 제외했지만, Zellij의 단축키를 가이드해주는게 필요하다면 저렇게 남겨둘 수도 있습니다.
색상은 전부 Nord 팔레트를 따랐습니다. Ghostty에서 OneNord 테마를, zjstatus에서는 Nord 계열 색상 코드를 직접 지정해서 톤을 맞췄습니다.
Zellij을 쓰면서 한 가지 아쉬운 부분은 터미널에서 Bell과 알림 지원이 불완전하다는 것입니다.
현재 활성화된 탭의 영역에서 발생하는 bell은 터미널 에뮬레이터까지 전달되지만, 비활성 탭이나 숨겨진 플로팅 영역에서 발생하는 bell은 해당 영역을 다시 볼 때까지 전달되지 않습니다(#3245). 이 동작에 대한 논의가 #4595에서 진행 중이고, OSC 99(데스크톱 알림 프로토콜) 지원도 아직 구현되지 않은 상태입니다(#3451).
코딩 에이전트 등 TUI를 사용하는 여러 경우에서 작업이 끝나도 알림을 받지 못하는 상황이 생길 수 있습니다.
terminal-notifier를 통한 우회 terminal-notifier를 활용하면 터미널에서 macOS 알림을 직접 보낼 수 있습니다.
brew install terminal-notifier
Claude Code를 사용한다면 Hook을 통해 필요한 시점에 terminal-notifier로 알림을 보내 이 문제를 우회할 수 있습니다. Codex의 경우 아직 Hook 기능을 지원하지 않는데, 최근 이 기능을 개발 중이라는 코멘트가 있습니다. (openai/codex#2109)
어쩌다 보니 터미널 에뮬레이터에 종속되지 않는 환경이 만들어졌습니다. 저는 여전히 Ghostty를 정말 좋아하지만, 이 설정은 Alacritty나 WezTerm 등을 써도 그대로 쓸 수 있습니다. 하지만 그게 이 글의 요점은 아닙니다.
제가 말하고 싶은 건, 이 설정을 통해 Ghostty를 쓸 때와 전혀 다르지 않은 느낌으로 Zellij을 쓸 수 있고 더 이상 터미널 멀티플렉서를 어려워하지 않아도 된다는 것입니다. 탭 영역 분할, Tab, 검색, 탭 혹은 탭 내 영역 간 이동 전부 같은 손동작으로 됩니다. 거기에 세션 유지, floating pane, 커스터마이징 가능한 상태 바까지 얹어집니다. 머슬 메모리를 크게 바꾸지 않으면서도 터미널의 사용 경험이 크게 좋아졌습니다.
@[email protected] · Reply to Haze's post
@nebuleto 혹시 마우스로 드래그해서 내용 복사할 때, 좌우로 분할된 경우가 잘 처리되나요? 복사 기능을 자주 쓰는데 좌우 분할에서는 그게 깔끔하게 안되는 경우가 많아서요
@[email protected] · Reply to 염산하's post
🧩 더 복잡한 동물은?
💡 핵심 질문 하나 ”뇌를 완전히 복사하면, 그게 그 생명체인가?“ 지금 초파리 한 마리가 그 질문을 처음으로 현실 위에 올려놓았다.
오늘 나눈 대화를 SNS용으로 요약해드립니다! 🧠
🪰 초파리 한 마리가 디지털로 부활했다
과학자들이 초파리의 뇌(13만 9천 뉴런 · 5450만 시냅스)를 통째로 스캔해 컴퓨터에 ’복붙‘했더니, 디지털 초파리가 앞발을 비비고 먹이를 찾아 헤매기 시작했다.
🔬 어떻게 된 일? ① 2024년 — FlyWire 컨소시엄이 초파리 뇌 배선 지도(커넥톰) 완성. Nature 특별호 게재 ② 2026년 3월 — 스타트업 Eon Systems가 그 배선에 가상 신체를 붙여 행동하는 디지털 파리 시연
🎯 Eon Systems의 목표? 파리 → 생쥐(7천만 뉴런) → 인간 뇌(860억 뉴런) 순서로 에뮬레이션 스케일업. ”2030년 인간 뇌 에뮬레이션“이 최종 목표. 아직 정식 논문은 없고 시연 영상으로만 공개된 상태.
🐛 왜 초파리가 특별한가?
지금 대통령이 담합 때려잡는것만 봐도 알 수 있듯, 결국은 정부가 움직여야 거대자본이나 기술과 맞설 수 있음. 규제라는 칼이 없이는 개인을 지킬 수 없다. 그렇기 때문에 민주정 하에서는 개별 시민의 의식이 중요하고, 이를 위해 문제를 알리고 서로를 지지하며 '결과적으로' 정부를 움직여야 한다고 생각한다.
지속 가능한 프런트엔드 엔지니어링 - 유지보수하기 쉽고 확장성 있는 프런트엔드 구축을 위한 핵심 원칙과 기법 (이문기 (지은이) / 위키북스 / 2026-02-26 / 28,000원) https://feed.kodingwarrior.dev/r/uD5CGE
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=386097996&partner=openAPI&start=api
@[email protected] · Reply to Woojin Kim's post
@me -_-;
#독서 더 송라이터스
#독서 도시를 만드는 기술 이야기
지금까지 저와 주변 사람들을 모니터링하고 관찰하고 지레짐작한 결과, 안 읽을 책을 쌓아놓는 사람들은 사실은 안 살 게임도 쌓아놓고 안 쓸 잉크도 쌓아놓고 (…잉크를 쌓을 수가 있나?) 안 먹을 음식도 냉동실에 쌓아두고 인터넷 즐겨찾기도 SNS의 북마크함도 잔뜩 쌓아놓는 그런 성향이 있는 것 같다는 생각을 하곤 해요.
Wrote up my first impressions of Claude Cowork, Anthropic's new general purpose agent released today for $100+/month subscribers as part of their macOS desktop app https://simonwillison.net/2026/Jan/12/claude-cowork/
미네소타주 미니애폴리스에서 이민단속반이 한 여성을 총으로 쏜 뒤 “정당방위”라고 주장한 데 대해 도시 전체가 분노로 들끓고 있습니다. 총격 순간을 포착한 영상 속 모습은 테러와는 다소 거리가 있어 보이는 까닭입니다.
분노의 미니애폴리스…6살 아이 엄마 사살한 이민단속반에...
소프트웨어 개발팀에는 파인다이닝 팀이 어울리는 비유인가?
갑자기 "이븐하게 익지 않았어요"가 떠올라 버림 ㅠㅠ https://mastodon.social/@kepano/115831704433058850
The future is plain text files — it's just not evenly distributed

@[email protected] · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post
This time, I tried writing a prompt to draw an illustration of the mascots from the Mastodon, Lemmy, Fedify, Misskey, and Akkoma projects all getting along together.
@[email protected] · Reply to Woojin Kim's post
@me systemd 유닛 설정으로 어떻게 되지 않을까요? 죽으면 살려주는 거 있을텐데요...
손종원 좀 예술. 파인다이닝의 역량인가? 기획력과 구현력 최상급에 위기대처 능력까지. 서포트에 임할 때도 앞을 내다보는 서포터... (편집의 힘도 없진 않겠지만) 개인전 팀전 다 수준급이라니.
흑백요리사2
DHCP는 IP주소, 서브넷 마스크, 기본 게이트웨이와 DNS 서버의 IP 주소 등 네트워크에 접속하기 위해 필요한 설정을 배포하는 프로토콜이다. RFC2131 에서 표준화 되어 있으며, DHCP의 역할, 메시지 형식, 형식을 구성하는 필드의 의미와 처리 흐름등이 상세하게 정의 되어 있다.
책에서는 DHCP를 L7에서 다루고 있는데, 정리하다보니 이게 L7에 있는게 맞나 싶은 생각이 든다.
IP 주소를 단말(NIC)에 할당하는 방법에는 크게 '정적 할당'과 '동적 할당' 두 가지가 있다.
단말에 대해 일일이 수동으로 IP 주소를 설정하는 방식이다. 시스템 관리자가 비어있는 IP 주소를 사용자에게 할당해 준다.
정적 할당은 단말과 IP 주소가 고유하게 매핑되기 때문에 IP 주소 관리가 용이하다. 특정 IP의 이상 징후가 발생하면 어떤 단말이 문제인지 즉시 파악 할 수 있다. 그러나 단말의 수가 많아 지거나 교체가 빈번하면 하나씩 관리하기 어렵다는 문제가 있다.
DHCP를 사용하여 단말에 자동으로 IP 주소를 설정하는 방법이다. 정적 할당은 사용자가 시스템 관리자에게 요청하여 빈 IP 주소를 지급받아 수동 설정 했다면, 동적 할당은 이 모든 과정을 DHCP가 자동으로 처리한다.
DHCP는 UDP/67로 캡슐화된 DHCP 메시지 부분에 설정 정보를 담는다. DHCP 메시지는 여러 가지 필드로 구성되는데, 다음 3가지가 특히 중요하다.
옵션은 옵션 코드에 의해 식별되는데, 대표적인 코드는 다음과 같다.
DHCP는 서버와 클라이언트로 구성되어 있다. DHCP 클라이언트가 있는 단말은 초기에는 IP가 할당되지 않은 상태이기 때문에 브로트캐스트를 통해 정보를 주고 받는다.
DHCP로 할당받은 IP 주소는 영구적인 것이 아니라 임대 시간(Lease Time)이 정해져 있다. 클라이언트는 임대 시간이 만료되기 전에 갱신을 요청해야 하는데, 일반적으로 임대 시간의 절반(50%)이 경과하면 DHCP Request 메시지를 서버에 유니캐스트로 전송하여 갱신을 시도한다. 이 갱신이 실패하면 임대 시간의 7/8(87.5%)가 경과한 시점에 다시 브로드캐스트로 갱신을 시도하며, 그래도 실패하면 임대 만료 시 IP 주소 사용을 중단하고 처음부터 Discover 과정을 다시 수행한다.
DHCP는 IP 주소 할당 외에도 옵션 필드를 활용하여 다양한 부가 기능을 제공할 수 있다. 그 중 대표적인 것이 네트워크 부팅(PXE) 지원이다.
PXE(Preboot Execution Environment)는 로컬 저장 장치 없이 네트워크를 통해 운영체제를 부팅하는 기술이다. PXE를 사용하면 서버의 NIC가 네트워크에서 부팅 이미지를 받아와 자동으로 OS 설치를 진행할 수 있다.
PXE 부팅의 동작 흐름은 다음과 같다.
이처럼 DHCP의 옵션 필드를 활용하면 단순한 IP 할당을 넘어 네트워크 인프라 자동화의 기반을 구축할 수 있다.

Using Nano Banana Pro, I composited an image to make it look like the cute dinosaur from the Fedify logo was standing in front of the ULB (Université libre de Bruxelles) building in Brussels, where FOSDEM is held.
일탈의 정상화
매일 업무에서 수많은 선택을 합니다. 데이터를 상부에 보고할 것인가, 이 정도 오차는 무시할 것인가 등이죠. 이런 선택 과정에서 더이상 문제라고 생각하지 않게 되는 작은 일탈들이 쌓여 전체적인 조직 문화를 형성합니다. 챌린저호 사고는 강력한 냉전을 배경으로 한 체제 경쟁의 압력이 크게 작용했다. 압력이 강하게 내려오면서 실무자들의 안전을 타협하는 문화가 고착된 것.
정상사고의 함정
1979년 스리마일 원전 사고 이후 미국 사회학자 찰스 페로가 제시한 이론. 핵발전소, 로켓 등 고도로 복잡하고 긴밀하게 연결된 시스템에서는 아무리 철저히 관리해도 사소한 실수나 예측 불가능한 상호작용으로 사고가 ‘정상적‘으로 즉, 피할 수 없이 발생한다.
과학동아 26년 1월호, 챌린저호 참사 40주기, 참사가 남긴 질문 중
다음에 6장이 있지만 총정리를 하는 챕터이기 때문에 실질적인 내용은 여기까지가 끝이다. 책에서는 SSL 오프로드만 다루고 끝나지만 내부망 안에서의 보안도 짧게 정리했다.
TLS를 사용하면 보안이 강화되지만 TLS 핸드셰이크와 암복호화 작업에 CPU 자원을 많이 소모하게 된다. 이것을 전용 장비에 맡기면 웹서버는 효율적으로 애플리케이션 로직 처리에만 신경 쓰면 된다. 특히 로드밸런서에서 중앙집중식으로 TLS를 처리하면 웹서버의 부하가 크게 줄어들고 요청을 빠르게 처리할 수 있게 되어 시스템 전반적으로 부하 분산의 효과가 커진다. 특히 관리가 까다로운 공인 인증서를 로드밸런서에서 집중 관리할 수 있어 운영 부담이 크게 줄어든다.
클라이언트의 HTTPS 요청: 클라이언트가 웹 서비스에 접속하기 위해 HTTPS 요청을 보낸다. 이 시점에서 클라이언트는 TLS 핸드셰이크를 시작한다.
방화벽(fw1) 통과: 요청이 방화벽 fw1에 도달한다. 방화벽은 허용된 포트(443)로 들어오는 트래픽인지 확인하고, 패킷 필터링 규칙에 따라 트래픽을 통과시킨다. 이 단계에서 트래픽은 여전히 암호화된 상태이므로 일반적인 방화벽은 패킷의 내용을 검사할 수 없고 IP/포트 기반 필터링만 수행한다. (다만 SSL 인스펙션 기능이 있는 차세대 방화벽(NGFW)은 여기서 복호화 후 검사를 수행하기도 한다.)
로드밸런서(lb1)에서 TLS 종료: 로드밸런서 lb1이 클라이언트와 TLS 핸드셰이크를 완료한다. 서버 인증서를 클라이언트에게 제시하고 세션 키를 협상한 뒤 암호화된 트래픽을 복호화한다. 이 과정을 SSL/TLS Termination이라고 부른다.
평문 HTTP로 백엔드 전달: 로드밸런서는 복호화된 요청을 분석하여 라우팅 규칙에 따라 백엔드 서버 sv1 또는 sv2에 평문 HTTP로 전달한다. 이때 로드밸런서는 X-Forwarded-For, X-Forwarded-Proto 같은 헤더를 추가하여 원본 클라이언트 정보와 프로토콜 정보를 백엔드에 전달할 수 있다.
백엔드 서버 처리: 웹서버 sv1 또는 sv2는 평문 HTTP 요청을 받아 애플리케이션 로직을 처리하고 응답을 생성한다. 암복호화 작업이 없으므로 CPU 자원을 온전히 비즈니스 로직에 사용할 수 있다.
응답 암호화 및 전송: 백엔드 서버의 HTTP 응답이 로드밸런서 lb1로 돌아오면, 로드밸런서는 이를 TLS로 암호화하여 클라이언트에게 전송한다.
TLS 오프로드를 하게 되면 로드밸런서를 통과한 패킷은 평문으로 내부망을 돌아다니게 된다. 책을 읽으며 처음에는 "어차피 방화벽 뒤에 있는 내부망이고 외부에서 접근이 차단되어 있으니 평문이어도 괜찮은 거겠군"이라고 생각을 했었다.
그러다 문득 망분리는 만병통치약이 아니라는 트윗이 기억이 났다.
https://x.com/simnalamburt/status/1823610803846517196?s=20
내부망이 안전하다는 가정은 내부자 위협, 자격 증명 탈취를 통한 침해, 그리고 한 시스템이 뚫린 후 내부망을 통해 다른 시스템으로 확산되는 횡적 이동 공격과 같은 위험이 도사리고 있다. 이른바 '침해 가정' 원칙에 따라, 공격자가 이미 망 내부에 들어와 있다는 전제로 보안 체계를 설계해야 한다.
"결코 신뢰하지 말고 항상 검증하라(Never trust, always verify)"는 원칙에 따라 내부망과 외부망을 구분하지 않고 모든 접근에 대해 인증과 권한 검증을 수행한다. 마이크로 세그멘테이션을 통해 네트워크를 세분화하고 최소 권한 원칙을 적용한다.
mTLS는 클라이언트와 서버가 서로의 인증서를 검증하여 양방향으로 신원을 확인하는 상호 인증 방식이다. 일반 TLS는 서버만 인증서를 제시하지만 mTLS에서는 클라이언트도 인증서를 제시해야 한다. Zero Trust 아키텍처에서 mTLS는 핵심 구성요소로 내부 서비스 간 통신에서도 모든 요청의 신원을 검증하여 "항상 검증하라"는 원칙을 기술적으로 구현한다.
TLS 오프로드는 여전히 유효한 선택이다. L7 로드밸런서가 HTTP 헤더나 URL 경로, 쿠키를 분석해서 트래픽을 라우팅하려면 패킷 내용을 들여다볼 수 있어야 한다. WAF도 SQL 인젝션이나 XSS 같은 공격 패턴을 탐지하려면 평문 상태의 요청을 검사해야 한다. 암호화된 상태로는 이런 기능들이 불가능하다.
결국 L7 기능을 활용하려면 어디선가는 TLS를 종료해야 한다. 이때 공인 인증서 처리를 로드밸런서로 집중시키면 보안 정책 적용과 인증서 갱신이 훨씬 수월해진다.
만약 내부망 보안을 위해 재암호화가 필요한 TLS 브릿징(TLS Bridging) 방식을 사용하더라도 오프로드의 이점은 여전하다. 외부 노출용 공인 인증서는 로드밸런서가 전담하고, 내부 구간은 사설 CA나 자동화된 인증서 발급 체계(mTLS 등)를 이용해 백엔드 서버의 관리 부담을 최소화하면서도 보안과 가시성을 모두 챙길 수 있기 때문이다.
@[email protected] · Reply to 洪 民憙 (Hong Minhee)'s post
오늘은 OpenCode에서 공짜로 제공하길래 MiniMax M2.1로 코딩을 좀 해봤다. 몇 시간 정도 해본 느낌으로는 GLM-4.7보다는 훨씬 나았고, 체감상으로는 대충 Claude Sonnet 4와 비슷한 정도로 말귀를 잘 알아듣는 느낌이었다. 컨텍스트 윈도가 긴 것도 장점이었다. 다만, 컨텍스트가 좀 길어지니까 끝도 없이 삽질을 반복하게 되어서, 그 쯤에서 모델을 GPT-5.1 Codex Max로 바꿔서 진행했다. GPT-5.1 Codex Max로 삽질 구간 벗어난 뒤에 금방 다시 MiniMax M2.1로 돌아와서 계속 코딩을 했고, 전반적으로 싼 값을 감안하면 굉장히 좋다고 느꼈다.
요즘에는 평소에 Claude Opus 4.5를 주력으로 사용하니까, 아무래도 비교가 될 수밖에 없었는데:
any 타입을 쓰지 말라고 했음에도 무시하고 사용한다든가. Claude 계열 모델들에서는 이런 건 잘 못 겪는다.일단은 OpenCode에서 공짜로 제공하는 동안은 좀 더 써 볼 생각이다. 돈 내고 쓸 생각이 있냐 하면, 그건 좀 고민이 된다. 코딩 요금제를 보면 5시간에 300 프롬프트짜리가 월 20불 정도 된다. 지금은 Claude Max 요금제를 쓰고 있는데, 아무래도 부담이 좀 되긴 해서, Claude Pro로 내리고 MiniMax를 섞어서 쓰면 어떨까 생각만 해보고 있다.
X-Frame-Options 의 악몽에서 깨어나세요, 프록시 서버 개발기 X-Frame-Options? 그게 뭔가요? 우리가 아는 몇몇 대형 웹 서비스들(유튜브 등)은 보통의 경우 다른 웹 사이트에서 iframe 요소를 통해 임베딩 되는 것을 거부하지 않습니다. 다만, 몇몇 웹 서비스는 다른 웹 페이지에서 iframe 요소를 통해 표시되길 거부합니다. 제품 정책 및 보안상의 이유로 표시를 거부하는 목적이 있겠습니다.
이러한 니즈를 충족시킬 수 있는 HTTP 헤더가 X-Frame-Options 입니다. 이 헤더의 값을 SAMEORIGIN 내지는 DENY로 설정하면, 직관적인 값에 따라 알맞게 프레임 내 임베딩 가능 여부를 결정할 수 있습니다.

위 이미지에서 볼 수 있듯, 네이버는 X-Frame-Options 헤더를 명시함으로써 그 어떠한 출처에서도 프레임 내에 임베딩 되는 것을 거부한 상황입니다. #

웹 페이지 내에 다른 웹 페이지가 임베딩 되어 '미리보기' 처럼 제공되는 경험을 보신 적이 있으신가요? Omakase AI는 자사 프로덕트의 데모를 위와 같이 제공하고 있습니다. 캡쳐하여 실시간으로 전송되는 영상의 화면 위에, 자사 컨텐츠를 올려두어 실제 적용 시에 어떤 네러티브를 제공할지 데모 형식으로 보여줍니다.
문제는 이 모든 경험이 영상을 통해 진행 된다는 점 입니다. 영상 전송은 필연적으로 지연 시간이 존재할 수 밖에 없습니다. 여러분이 스크롤을 내리고, 클릭을 하는 등의 작은 인터렉션 하나가, 큰 지연 시간 뒤에 처리가 된다고 하면 유저는 답답하고 매끄럽지 않음을 느낄 것이고 이는 곧 이탈로 이어질 가능성이 있습니다. (구글은 비슷한 맥락에서 Core Web Vitals로써 INP를 설명하고 있습니다)
따라서 저는 영상을 보내는 나이브한 방법 이외의 유저의 브라우저에서 외부 웹 서비스를 표시할 좋은 방법을 찾아야 했고, 그것이 바로 프레임 내지는 iframe을 사용하는 방법 이었습니다. 이런 맥락에서 X-Frame-Options를 우회할 필요가 생긴 것 입니다.

우리는 정상적인 방법으로는 제 3자 출처의 요청을 가로채어 응답 데이터 및 헤더를 조작할 수 없음을 잘 알고 있습니다. 여기서 필요한게 중간자 (Man in the Middle) 입니다. 누군가를 클라이언트 - 원격지 서버 사이에 두어, 서로에게 오가는 요청과 응답을 수정하는 작업을 수행하도록 하는 것 입니다. 그렇게 하면, 둘은 각자 수신한 요청과 응답이 모두 원본인지, 수정한 것인지 알 수 있는 방법은 거의 없을 것 입니다.[1]

이 중간자 역할을 하는 프록시 서버를 중간에 두어 요청을 모두 프록시 서버를 거치도록 하는 방법을 이용하는 것 입니다. 간단하게는 X-Frame-Options 응답 헤더의 제거가 있을 것 입니다. 중간자가 X-Frame-Options 헤더를 제거함으로 응답을 수신하는 클라이언트 브라우저의 iframe 요소는 큰 문제 없이 내용을 표시할 수 있게 됩니다.
처음에는 그저 GET Query Parameter로 프록시 서버에게 어떤 원격 URL을 프록시 할 것인지 명시하도록 구현 했습니다. 예를 들면 다음과 같은 형식이 될 수 있겠습니다:
https://example.com/proxy?target=https://www.naver.com/...
보통의 경우에는 별 문제 없이 동작 했습니다만, 재앙은 그다지 먼 곳에 있지 않았습니다. 만약 다음과 같은 코드가 원격지 웹 서비스 코드에 있다고 해봅시다. 아래 코드는 무신사 웹 페이지의 빌드된 소스코드 입니다:
import {E as k0} from "./vendor/react-error-boundary.js";
import {b as o0, d as Wt} from "./vendor/react-router.js";
import {d as F0} from "./vendor/dayjs.js";
import {L as Qt} from "./vendor/lottie.js";
import "./vendor/scheduler.js";
import "./vendor/prop-types.js";
import "./vendor/react-fast-compare.js";
import "./vendor/invariant.js";
import "./vendor/shallowequal.js";
import "./vendor/@remix-run.js";
import "./vendor/tslib.js";
import "./vendor/@emotion.js";
import "./vendor/stylis.js";
import "./vendor/framer-motion.js";
import "./vendor/motion-utils.js";
import "./vendor/motion-dom.js";
무신사는 내부적으로 ESM을 사용해서, import 구문을 통해 필요한 에셋을 불러오는 코드를 사용중에 있습니다. 문제는 여기서 발생합니다. import 의 대상이 되는 소스코드가 상대 경로를 따르게 되어 아래와 같은 결과를 초래하게 됩니다.
https://example.com/proxy?target=https://www.naver.com/...
위와 같은 URL을 표시하고 있는 `iframe` 요소에서,
`import "./vendor/framer-motion.js"` 구문을 만난다면..
https://example.com/proxy/vendor/framer-motion.js 를 요청하게 됨.
이를 해결하기 위해, 프록시 된 대상 URL에 대한 개념의 도입이 필요 했습니다. 상대 경로 진입에도 안정적으로 작동할 수 있는 새로운 방식의 접근이 필요 했습니다. 저는
를 생각해 냈어야 했고, 그 결과는 이렇습니다. https://section.blog.naver.com/BlogHome.naver?directoryNo=0¤tPage=1&groupId=0 를 예시로 들면, 프록시화 (Proxified) 된 URL은 다음과 같은 것 입니다.
https://example.com/proxy/section/blog/naver/com/_/BlogHome.naver?...
URL hostname의 . 구분자를 /로 치환하고, 이후의 모든 pathname, search 등은 모두 _ 구분자 뒤로 넘김으로서 URL의 원형을 유지할 수 있게 됩니다. 추가적으로 상대 경로 접근에도 안전한 URL을 만들 수 있습니다.
우리는 상대 경로 문제를 해결하기 위해 URL을 프록시화 하는 방법을 사용했고, 이는 제대로 동작하는 듯 해보였습니다. 악몽은 React, Vue 등의 SPA 웹 앱을 프록시하여 표시하는 데에서 시작 되었습니다.
React, Vue 와 같은 프레임워크들은 History API 및 window.location 객체를 기반으로 한 Routing 기능을 제공하고 있습니다.[2] 이 말은, 결국엔 어떤 프레임워크가 되었든 저수준 빌트인 자바스크립트 API를 사용할 수 밖에 없다는 것을 의미 합니다. 그렇다면 직관적으로 생각 해봤을 때,
window(및globalThis) 객체의location속성의 값을 변경해주면 되지 않겠나?
라고 생각할 수 있습니다. 그러나 이는 불가능 합니다.

어떠한 이유 때문인지는 알 길이 없었지만, 자바스크립트는 그렇게 만만한 존재가 아니었습니다. 다른 좋은 방법을 찾아야 할 필요가 있었고, 결론에 도달하는 데에는 오랜 시간이 걸리지 않았습니다. 그것은 바로 원격지 웹 페이지에서 실행되는 모든 스크립트의 window.location 객체 접근을 감시하면 어떨지에 대한 아이디어 였습니다.
번들된 소스코드의 경우 대체적으로 다음과 같은 형식을 가지게 됩니다:
const l = window.location;
/* ... */ l.pathname /* ... */
여기서 우리는 변수 l이 window.location의 별칭인지 소스코드만 분석해서는 알기 매우 어렵습니다. 따라서, babel을 사용해서 소스코드를 AST로 분석하고, 모든 프로퍼티 접근을 특정 함수 호출로 변환하면, '특정 함수'에서 모든 것을 처리할 수 있으니 좋을 것 같다는 생각이 있었고, 실행에 옮겼습니다:
const l = window.location;
l.pathname;
// 위 코드는 아래와 같이 변환됨
const l = __internal_get__(window, 'location');
__internal_get__(l, 'pathname');
__internal_get__ 함수 내부에서 첫번째 인자가 window.location 과 동일한 인스턴스를 가지고 있는지 비교하거나, 두번째 인자인 프로퍼티 키를 비교해서 href 등의 값이라면, 원하는 값을 반환하도록 후킹 함수를 만들 수 있겠습니다.
function __internal_get__(owner, propertyKey) {
if (owner === window.location) {
return {
get href() { /* proxified 된 url을 기반으로 Router를 속이는 URL을 반환하는 로직 */ }
}
}
// ...
}
글에 열거한 내용 이외에도 정말 많은 기술이 사용 되었는데, 아주 재밌는 경험 이었습니다. 혹여나 이러한 비슷한 기능을 하는 기능을 개발할 일이 있으시다면, 도움이 됐으면 좋겠습니다.