당신이 .gitignore 에 *.png 한 줄을 넣어 디자인 원본 그림이 커밋에 섞이지 않게 해 두었다고 하자. 몇 달 뒤 Next.js 앱 폴더에 icon.png 와 opengraph-image.png 를 넣었더니 로컬에서는 파비콘이 잘 뜨는데, 배포하면 아이콘이 안 보임. 오류는 한 줄도 없다. 개발 서버 포트를 비우려고 lsof -i :3000 결과를 그대로 kill 했더니 띄워 둔 브라우저가 같이 죽는 일, 셸 스크립트의 잡음을 줄이려고 붙인 2>/dev/null 때문에 grep 이 아무것도 안 잡힘, 운영 GA4 속성에 로컬 QA 트래픽이 섞이는 일도 같은 뿌리다.
넓게 건 패턴은 왜 의도 밖 대상까지 잡나
고르는 조건을 겉 이름(확장자·포트 번호·출력 스트림·빌드 모드)으로 걸면, 같은 이름을 가진 의도 밖 대상이 조용히 함께 잡힌다. 당신이 원한 것은 "디자인 원본", "그 포트를 연 서버", "필요 없는 경고", "운영 사이트의 방문"이었다. 그런데 조건은 그 성질이 아니라 그 성질과 대체로 겹치는 이름에 걸려 있다. 겹치지 않는 부분이 생기는 순간 패턴은 아무 말 없이 그것까지 집어 간다. 빠진 것은 오류가 아니라 "없는 것"이라, 알려 주는 곳이 없다.

흔한 네 얼굴
- 무시 목록(ignore):
*.png,*.log,build/같은 줄은 그 이름을 가진 모든 파일에 걸린다. Next.js 처럼 파일 이름이 곧 설정인 프레임워크에서는icon.png가 코드다. 로컬 디스크에는 있으니 개발 중엔 정상이고, 커밋에서만 빠진다. - 프로세스 고르기:
lsof -i :PORT는 그 포트를 연 프로세스만이 아니라 그 포트에 접속 중인 프로세스도 보여 준다. 브라우저 탭이 개발 서버에 붙어 있으면 브라우저도 목록에 있다. - 출력 버리기: 많은 CLI 가 사람이 읽을 표를 표준 출력(stdout)이 아니라 표준 오류(stderr)로 쓴다. 배포 목록을 보여 주는
vercel ls가 그렇다.2>/dev/null은 "경고"가 아니라 "2번 통로 전부"를 버린다. 폴링 루프의 grep 이 영영 못 찾고 끝까지 돈다. - 환경 나누기: GA 측정 ID 를
.env.production에 두면 "운영"이 아니라 프로덕션 빌드 모드에 걸린다. 로컬에서next start로 돌린 QA, 미리보기(preview) 배포도 프로덕션 빌드다. 배포 전 QA 를 꼼꼼히 할수록 운영 수치가 더 오염된다.
조건은 어떻게 좁히나
원하는 대상을 이름이 아니라 그것만 가진 성질로 다시 말해 보고, 그 성질을 조건에 넣는다.
# 무시 목록 — 이름 대신 위치로, 규칙 파일에는 예외를 명시
/design/**/*.png
!app/**/icon.png
!app/**/apple-icon.png
!app/**/opengraph-image.png
# 포트를 연 서버만 — LISTEN 상태로 거른다
lsof -t -i :3000 -sTCP:LISTEN
# CLI 출력을 읽을 때는 버리지 말고 합친다
vercel ls 2>&1 | grep Ready
GA4 처럼 "운영"이 성질인 경우에는 빌드 모드가 아니라 실제 운영 도메인으로 거른다. 환경변수 분기만으로는 로컬 프로덕션 빌드와 미리보기 배포를 못 가른다.
// 운영 도메인에서만 측정을 켠다
if (location.hostname === 'example.com' || location.hostname === 'www.example.com') {
gtag('config', GA_MEASUREMENT_ID);
}
행동하기 전에 무엇을 확인하나
좁힌 조건도 틀릴 수 있다. 지우기·죽이기·버리기·보내기 같은 되돌리기 어려운 행동 앞에서는 고른 목록을 먼저 눈으로 본다.
- 무시 목록을 고치거나 새 파일 규칙 리소스를 넣었으면
git status와git check-ignore -v <파일>로 실제로 추적되는지 본다 - 프로세스를 죽이기 전에
kill없이 목록만 찍어 본다(lsof -i :3000 -sTCP:LISTEN). 명령 이름 칸에 브라우저가 있으면 멈춘다 - 출력을 버리는 리디렉션을 붙이기 전에, 버릴 통로에 실제로 무엇이 나오는지 한 번 본다(
cmd 2>&1 >/dev/null은 stderr 만 보여 준다) - 분석 도구를 붙였으면 로컬·미리보기 주소에서 페이지를 열고 실시간 보고서에 안 찍히는지 본다. GA4 는 특정 날짜 데이터를 통째로 지우는 기능이 없다 — 데이터 삭제 요청은 매개변수 값만 지운다. 오염됐으면 보고 시작일을 다음 날로 옮기는 수밖에 없다
우리가 겪은 일
- 아이콘이 배포에서만 없었다. 저장소의
*.png무시 줄이 Next.js 앱 폴더의icon.png·apple-icon.png·opengraph-image.png를 커밋에서 뺐다. 로컬은 정상이었고,git status에 그 파일들이 안 보이는 것으로 알았다. - 포트를 비우다 브라우저가 죽었다.
lsof -i :PORT결과를 그대로kill하자 개발 서버에 붙어 있던 브라우저 헬퍼 프로세스까지 죽었다.-sTCP:LISTEN으로 거르면 서버만 남는다. - 배포 대기 루프가 끝까지 돌았다.
vercel ls가 표를 stderr 로 내는데2>/dev/null을 붙여 grep 이 한 번도 "Ready" 를 못 봤다. 세 번의 세션에서 같은 루프가 시간 상한까지 돌았다. - QA 트래픽이 운영 수치에 섞였다. 측정 ID 를
.env.production에 둔 채 배포 전 QA 를 로컬 프로덕션 빌드로 돌려, 그날 운영 속성의 실시간 보고서에 우리 방문이 찍혔다. 이후 운영 도메인일 때만 측정을 켜도록 코드로 막았다.
네 사례 모두 명령은 성공했고 오류는 없었다. 조건이 "원한 것"이 아니라 "원한 것과 이름이 같은 것"을 골랐을 뿐이다.
이전 글검사의 기준을 공격자 손에 두지 않는다 — 규칙·발급값·해석은 상대가 못 바꾸는 쪽에서 가져온다