
Claude Code가 이모지를 다시는 쓰지 못하게 하는 다층 게이트 (pre-commit + 리뷰 + 배포의 3층 구조로 기계적 차단)
요약
Claude Code 사용 시 발생하는 원치 않는 이모지 삽입 문제를 해결하기 위해 pre-commit, 리뷰, 배포 단계의 3층 구조로 기계적 차단 시스템을 구축하는 방법을 설명합니다.
핵심 포인트
- AI의 컨텍스트 망각으로 인한 이모지 혼입 문제 해결
- 프롬프트 의존 대신 pre-commit 등 자동화된 게이트 구축
- Node.js 스크립트를 활용한 단일 로직 관리 방식
- 매체 톤 유지 및 광고 심사 규정 준수를 위한 방어 전략
Claude Code(Anthropic 제작의 CLI 통합 AI 코딩 어시스턴트)를 사용하여 웹 매체 기사나 컴포넌트를 작성하게 하면, 요청하지 않았는데도 이모지가 혼입되는 경우가 있다.
예를 들어 절차를 작성하게 하면 문장 끝에 체크 마크 이모지가 붙는다. 제목에 경고 마크 이모지, 비교표의 행 끝에 별 마크 이모지가 붙는 식이다. 이쪽에서 "이모지를 사용하지 마세요"라고 말해도, 세션(Session)이 바뀌면 잊어버리고 재발한다.
참고로 이 기사 자체도 "이모지를 사용하지 않는다"라는 방침으로 작성하고 있다. 따라서 인용문의 코멘트나 문자열에 포함된 이모지 글리프(Glyph)는 본문에서 "(이모지)" 또는 문장 표현으로 바꾸어 인용하고 있음을 미리 밝혀둔다.
이것이 실제 운용에서 곤란한 이유는 여러 가지가 있다.
톤(Tone)이 무너진다. 텍스트 중심의 매체에서 갑자기 이모지가 나타나면 편집 방침의 일관성이 손상된다. 특히 돈이나 건강 등 "YMYL(Your Money or Your Life)" 영역의 기사에서는 시각적인 선동적 표현이 독자의 신뢰감을 떨어뜨린다.
광고 네트워크의 매체 심사에 영향을 준다. 제휴 프로그램(Affiliate Program) 중에는 "과장 표현 금지"를 매체 규약에 명시하고 있는 경우가 있다. 이모지 자체가 직접 문제가 될지는 케이스 바이 케이스(Case by case)지만, 심사 담당자의 인상 관리 차원에서 피하는 것이 무난하다.
수동 리뷰는 확실하지 않다. "이모지가 보이면 지적한다"라는 운용은 피곤할 때나 양이 많을 때 반드시 놓치게 된다. 애초에 생성물을 인간이 전문 육안으로 확인하는 비용은 AI를 사용하는 의의와 모순된다.
해결책으로서 "매번 프롬프트(Prompt)에 적는다"라거나 "세션 초반에 금지를 선언한다"라는 방법도 있다. 하지만 실제로는 AI가 긴 컨텍스트(Context) 속에서 규칙을 잊어버린다. 근본적인 대책은 AI가 이모지를 쓰더라도 그것이 리포지토리(Repository)나 서버에 도달하기 전에 기계적으로 막는 구조를 만드는 것이다.
사람에게 의존하지 않는 다층 방어(Defense in depth)의 사고방식은 정보 보안의 기본이다. 이것을 이모지 대책에 응용한다.
커밋 시 → pre-commit 후크 (.githooks/pre-commit)
리뷰 시 → npm run review / npm run preflight 시작 시 실행
배포 시 → deploy.ps1의 공개 게이트에서 실행
각 층의 역할과 "왜 그곳에 배치하는가"는 뒤의 섹션에서 자세히 설명한다.
체크 로직은 하나의 Node.js 스크립트(scripts/check-emoji.mjs)로 집약하여 3개 층 모두에서 호출한다. 로직이 분산되어 있으면 한쪽을 수정했을 때 다른 쪽이 옛날 상태로 남기 때문이다.
스크립트 서두의 주석에 방침이 적혀 있다(발췌).
/**
* 장식용 이모지 혼입을 검출하여 반드시 차단하는 게이트 (매체 톤 통일 · "다시는 사용하지 않음" 담보).
* ...
스캔 대상 디렉토리는 src 하위의 표시물로 한정하고 있다.
const TARGET_DIRS = ['src/content/articles', 'src/components', 'src/layouts', 'src/pages'];
src/content/articles가 기사 MDX 파일의 위치이며, 나머지 3개는 Astro 컴포넌트와 페이지 디렉토리다. node_modules나 scripts 자체는 대상에서 제외되므로, 이 스크립트의 주석 내에 있는 이모지 예시가 자기 검출에 걸리는 일은 없다.
const EMOJI_RE =
/[dz00}-ǺFFɠ0}-ɻF}ʰ0}-ʿF}ǰ00}-DzFF}]F?/gu;
Unicode의 이모지 블록을 4개의 범위(Range)로 지정하고 있다.
| 범위 | 대표적인 내용 |
|---|---|
| U+1F300 - U+1FAFF | 날씨 · 동물 · 음식 · 깃발 · 기호 계열의 이모지 |
| ... |
끝부분의 \u{FE0F}?는 이모지 변형 선택자(Variation Selector-16, VS16)의 옵션 지정이다. 이모지 뒤에 이 제어 문자가 이어지는 경우도 1건으로 계산한다.
중요한 점은, 비교표에서 자주 사용하는 ○ △ ◎ × 나 화살표 → 는 이 범위에 포함되지 않는다는 것이다. 이것들은 기호로서 검출되지 않는다. 이모지를 금지하면서도 기호에 의한 표 구성을 망가뜨리지 않기 위한 설계다.
.astro · .ts · .mjs
등의 코드 파일은 주석 행에 작성된 이모지를 화면에 출력하지 않는다. 따라서 코드 파일에서는 주석을 제거한 후 스캔한다. 기사 (.mdx)는 주석을 제거하지 않는다 (MDX 주석 {/* ... */} 내의 이모지도 만약을 위해 검출한다).
function stripComments(text) {
return text
.replace(/\/\*[\s\S]*?\*\//g, '') // 블록 주석 /* ... */
...
http:// 등의 URL이 행 주석(line comment)으로 잘못 제거되지 않도록, 직전이 :가 아닌 경우에만 //를 주석으로 간주하는 장치가 있다.
파일마다 .mdx인지 여부를 판정하여 적용한다.
const scanned = extname(file) === '.mdx' ? raw : stripComments(raw);
이모지가 단 1건이라도 발견되면, file:line 형식으로 전건을 출력한 뒤 process.exit(1)로 종료한다.
if (hits.length === 0) {
console.log('[PASS] 장식용 이모지는 발견되지 않았습니다.');
process.exit(0);
...
출력 예시 (이모지 글리프는 본 기사의 방침에 따라 문장 표현으로 대체함):
[STOP] 장식용 이모지가 2건 발견되었습니다 (체크 표시 등의 이모지는 사용하지 마세요).
→ ○ △ × 나 【NG】【OK】 등의 기호·문자로 대체해 주세요.
src/content/articles/some-article.mdx:42 [(이모지)] 절차가 완료되면 다음으로 진행합니다 (체크 표시 이모지)
...
개발자는 이 출력을 보고 해당 행을 ○나 【OK】 등의 문자 표현으로 수정하면 된다.
.githooks/pre-commit의 전체 내용은 다음과 같다.
#!/bin/sh
# 장식용 이모지(체크 표시나 불꽃 등의 이모지)의 혼입을 커밋 전에 차단한다.
# 활성화: git config core.hooksPath .githooks (클론 직후에 한 번만 실행)
...
왜 여기에 두는가. 커밋 전이 코드 흐름에서 가장 빠른 단계다. 여기서 차단하면 이모지가 포함된 코드가 리포지토리 히스토리에 남지 않는다. "나중에 고치면 돼"라는 미루기가 애초에 발생하지 않는다.
활성화는 한 번만. Git은 기본적으로 .git/hooks/를 참조하기 때문에, 리포지토리에 .githooks/를 두는 것만으로는 작동하지 않는다. 클론 후에 한 번만 다음 명령어를 실행해야 한다.
git config core.hooksPath .githooks
이 명령어는 글로벌 설정이 아니라 리포지토리 로컬 설정이므로, 다른 리포지토리에는 영향을 주지 않는다.
긴급 회피. 어떻게든 이모지를 포함한 채로 커밋해야 하는 특수한 사정이 있는 경우 (예: 이모지 취급을 논의하는 Issue 관련 커밋 등)에는 git commit --no-verify로 회피할 수 있다. 훅의 주석에도 기재되어 있다. 단, 남용하지 말 것.
package.json의 scripts 섹션 (발췌).
{
"review": "node scripts/check-emoji.mjs && node scripts/check-articles.mjs",
"preflight": "node scripts/check-emoji.mjs && node scripts/check-articles.mjs && node scripts/check-freshness.mjs"
...
&&로 체이닝(chaining)되어 있기 때문에, 이모지 체크가 실패한 시점에서 후속 체크는 실행되지 않는다. "이모지가 남아있는 동안에는 리뷰가 완료되지 않는" 상태가 된다.
왜 여기에 두는가. 기사를 초안(draft)에서 공개 상태(draft: false)로 전환하기 전에 반드시 npm run review를 실행하는 워크플로우로 되어 있다. 이 단계에 이모지 체크를 포함함으로써, pre-commit을 빠져나간 케이스 (예: 다른 도구에서 직접 파일이 생성됨, 훅 활성화를 잊음)를 커버할 수 있다.
preflight
preflight는 정보의 최신성 검증(정보가 오래되었는지 확인)까지 포함하는 더욱 엄격한 명령이다. 배포 직전의 최종 확인에 사용한다.
scripts/deploy.ps1의 공개 게이트 부분(발췌).
# --- 1.5. 공개 게이트: 컴플라이언스 체크 (Compliance Check) ---
if (-not $SkipChecks) {
# 장식 이모지 게이트 (장식 이모지의 혼입을 운영 환경 반영 전에 반드시 차단)
...
왜 여기에 두는가. 제1층, 제2층을 빠져나갔을 경우를 대비한 최종 방파제다. 어떤 이유에서든 이모지가 포함된 콘텐츠가 운영 서버에 공개되는 것을 물리적으로 방지한다. throw로 예외를 던지기 때문에, 배포 스크립트 전체가 중단된다.
-SkipChecks 플래그를 지정한 경우에만 회피할 수 있다. 긴급 배포 시의 탈출구(Escape Hatch)로서 존재하지만, 사용할 때는 팀에 대한 설명 책임이 발생하는 설계로 되어 있다.
"1층만으로 충분하지 않나?"라는 의문이 생길 수도 있다. 실제로는 그렇지 않다.
pre-commit 훅은 core.hooksPath를 설정하지 않으면 작동하지 않는다. 신규 멤버가 설정 절차를 건너뛰면 무효화된다. 또한, CI나 스크립트에서 파일을 직접 생성하여 커밋하는 자동화 흐름에서는 --no-verify가 사용되기도 한다.
npm run review는 개발자가 의식적으로 실행하지 않으면 돌아가지 않는다. 피곤할 때나 서두를 때 스킵될 가능성이 있다.
배포 게이트는 강력하지만, "운영 배포 시점에 처음으로 에러가 발생하는" 상황은 피드백 루프(Feedback Loop)가 너무 길다. 커밋 직후에 알 수 있는 편이 훨씬 수정 비용이 낮다.
3층을 겹침으로써 "어느 한 층이 뚫리더라도 다른 층이 막는다"라는 중복성(Redundancy)이 생긴다. 이는 보안의 다층 방어(Defense in Depth)와 같은 개념이다.
기존 Node.js 프로젝트에 최소 구성으로 도입하는 절차를 보여준다.
scripts/check-emoji.mjs를 생성하고, 다음 내용을 작성한다 (실제 구현은 서두에서 설명한 바와 같다).
import { readdir, readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join, relative, extname } from 'node:path';
...
스캔 대상인 TARGET_DIRS는 자신의 환경에 맞춰 변경한다. Astro 이외의 프레임워크라면 src/app, pages, components 등이다.
.githooks/pre-commit을 생성한다 (Windows에서도 .githooks라는 디렉토리 이름으로 작동한다).
#!/bin/sh
node scripts/check-emoji.mjs
if [ $? -ne 0 ]; then
...
생성 후, 훅에 실행 권한을 부여한다 (macOS / Linux).
chmod +x .githooks/pre-commit
Windows의 git-bash 환경에서는 chmod가 필요 없는 경우가 많지만, WSL을 경유하는 경우에는 필요하다.
git config core.hooksPath .githooks
팀에서 사용하는 경우, README.md나 CONTRIBUTING.md의 설정 절차에 이 명령어를 기재해 둔다.
{
"scripts": {
"check:emoji": "node scripts/check-emoji.mjs"
...
npm run check:emoji로 단독 실행할 수 있도록 해둔다. 리뷰 명령어가 있다면 그 앞부분에 통합한다.
{
"scripts": {
"check:emoji": "node scripts/check-emoji.mjs",
...
테스트를 위해 임의의 파일에 이모지를 한 글자 쓴 다음 npm run check:emoji를 실행하여, [STOP]이 나오고 종료 코드(Exit Code) 1이 되는지 확인한다. 확인 후에 이모지를 삭제하고 [PASS]가 되는지 확인한다.
AI가 작성하는 코드나 문장에 대한 "규칙 준수 강제"는 프롬프트가 아니라 기계적인 게이트로 담보하는 것이 더 확실하다.
이번에 구현한 3층 게이트의 설계 원칙은 다음과 같다.
- 체크 로직은 하나의 스크립트로 집약하여 여러 계층에서 호출한다
- 각 계층은 "왜 그 시점에서 차단하는가"에 대한 문맥이 다르다 (커밋 히스토리를 더럽히지 않기 위해 / 초안 공개 전에 막기 위해 / 운영 환경 도달 전의 최종 방파제 역할)
- 어느 한 계층이 무효화되더라도 다른 계층이 차단하는 중복성(Redundancy)을 갖는다
- 탈출구(
--no-verify,-SkipChecks)는 존재시키되, 의도적인 조작이 필요한 형태로 만든다
AI 생성 콘텐츠를 운영에 도입할 경우, "AI가 지켜줄 것"에 의존하지 않고 "지키지 못했을 때 멈추는 메커니즘"을 먼저 만드는 것이 중요하다. 이모지 사례는 작아 보일 수 있지만, 동일한 메커니즘을 스팸성 키워드 혼입 탐지, 외부 링크 도메인 제한, 특정 표현 금지 등 폭넓은 용도로 응용할 수 있다.
- Node.js 공식 문서 —
node:fs/promises - Git 공식 문서 — githooks
- Unicode Emoji Chart — https://unicode.org/emoji/charts/full-emoji-list.html
마찬가지로 "AI에게 맡긴 작업의 품질을 기계적으로 담보한다"는 주제로 작성한 기사입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기