정의
**단어 분리(word splitting)**는 셸(bash·zsh 같은 명령 해석기)이 변수나 명령 치환을 확장한 뒤, 그 결과 문자열을 공백·탭·개행 기준으로 여러 개의 인자(argument)로 쪼개는 동작이다. **인용(quoting)**은 값을 큰따옴표 "..."로 감싸 이 분리를 막고, 값 하나를 인자 하나로 넘기게 하는 것이다.
한마디로: 변수에 담긴 값이 맞아도, 인용하지 않으면 셸이 그 값을 여러 조각으로 나눠 명령에 넘긴다.
왜 필요한가
경로에 공백이 든 상황을 생각해 보자. DIR="/tmp/My Files" 를 ls $DIR 로 쓰면, 셸은 $DIR을 확장한 뒤 공백에서 쪼개 ls /tmp/My Files 로 넘긴다. ls는 존재하지 않는 두 경로를 받고 "그런 파일 없음"으로 실패한다. 값(echo "$DIR")은 분명 맞는데 명령만 실패하는, 초심자를 오래 붙잡는 증상이다.
훅(hook) 스크립트나 CI 설정처럼 사람이 자주 들여다보지 않는 곳에서 이 실수는 조용히 깨진다. 홈 디렉터리 경로, 플러그인 루트, 사용자가 만든 파일명 등 공백·특수문자가 섞일 수 있는 값이 실제로 흔하기 때문에, 이 규칙을 모르면 반복해서 걸린다.
동작 원리
셸이 한 줄을 실행하기까지 대략 이런 순서를 밟는다.
- 확장(expansion):
$VAR,${VAR},$(cmd)등을 실제 값으로 치환한다. - 단어 분리(word splitting): 확장 결과를
IFS(Internal Field Separator, 기본값은 공백·탭·개행) 문자를 기준으로 여러 토큰으로 나눈다. 이 단계는 인용되지 않은 확장에만 적용된다. - 경로명 확장(globbing):
*,?같은 패턴을 파일명으로 확장한다(역시 인용 안 된 경우만). - 최종 토큰들을 명령의 인자로 전달한다.
"$VAR"로 감싸면 2·3단계가 통째로 생략되어 값이 인자 하나로 보존된다. 작은따옴표 '...'는 1단계(확장) 자체를 막아 리터럴로 취급한다.
실무 적용
정적 검사기 shellcheck는 인용 누락(SC2086 등)을 자동으로 잡아 준다. 스크립트를 CI에 넣기 전 한 번 돌리는 것이 가장 값싼 방어다.
트레이드오프
- 인용은 거의 비용이 없다. "항상 인용"을 기본으로 삼아도 잃는 것이 사실상 없다 — 예외는 의도적으로 분리가 필요한 소수 경우뿐.
- 의도적 분리가 필요할 때(예: 공백으로 구분된 옵션 목록을 인자로 펼치고 싶을 때)는 인용을 빼거나 배열을 쓴다. 다만 이때도 배열이 더 안전하고 명확하다.
사용하면 안 되는 경우
- 값에 공백·특수문자가 절대 없다고 확신할 수 없다면 인용을 빼면 안 된다. "지금은 괜찮다"가 미래의 파일명·경로에서 깨진다.
- 인용을 빼서 단어 분리에 의존하는 "여러 인자 펼치기"는 배열이 있는 셸에서는 안티패턴이다 —
"${arr[@]}"가 더 안전하다.
흔한 실수
$VAR를 인용 없이 명령에 넘김. 값은 맞는데 명령이 실패하면 가장 먼저 의심할 지점.- 빈 값 처리 누락:
cmd $EMPTY는 인자 0개가 되어 뒤 인자가 앞으로 밀린다."$EMPTY"는 빈 문자열 1개를 유지한다. - 명령 치환 미인용:
x=$(cmd)는 줄바꿈이 공백으로 접히고 분리된다."$(cmd)"로 감싼다. - 작은따옴표 안에서 변수 확장을 기대:
'$VAR'는 리터럴$VAR다. 값 삽입은 큰따옴표.
관련 개념
- serverless-stateless-execution — 실행 환경마다 경로·상태가 달라지는 맥락에서 인용 실수가 드러나기 쉬움