---
title: "macOS의 sort·uniq은 UTF-8 로케일에서 한글을 전부 같은 값으로 본다"
description: "macOS의 sort와 uniq 명령어가 UTF-8 환경에서 한글 등 비ASCII 문자를 동일하게 취급해 데이터를 유실하는 문제를 설명하고 LC_ALL=C를 통한 바이트 단위 비교 해결책과 검산의 중요성을 제안합니다"
date: 2026-08-27
updated: 2026-09-01T03:17:46.072Z
tags: [shell, macos, locale, unix]
canonical: https://blog.wooncloud.com/posts/macos-sort-uniq-cjk-locale-trap
---

![서로 다른 두 물체를 같은 무게로 재는 저울](/images/posts/macos-sort-uniq-cjk-locale-trap/ddacb67c-4e4c-479e-bf0b-123cc2a0eb25.webp)

git 로그에서 작성자별 커밋 수를 세는, 어디서나 쓰는 한 줄짜리 집계를 돌렸다.

```bash
git log --pretty=format:'%an' | sort | uniq -c | sort -rn
```

결과는 `195 홍길동` 한 줄. 그런데 같은 데이터에 `grep -c` 를 걸면 다른 이름이 16건, 4건, 3건 분명히 나온다. 파이프 하나를 사이에 두고 사실이 뒤집힌 셈이다.

에러는 없었다. 종료 코드 0, 그럴듯한 표 한 장. 그대로 리포트에 실릴 뻔했다.

## 재현

범인은 `uniq` 이고, 조건은 **로케일**이다. macOS 기본값인 `LANG=en_US.UTF-8` 환경에서 세 글자를 넣어 보면 바로 드러난다.

```bash
printf '가\n나\n가\n다\n' > k.txt

LC_ALL=en_US.UTF-8 uniq -c k.txt
#    4 가          ← 서로 다른 세 글자가 한 값으로 합쳐졌다

LC_ALL=C uniq -c k.txt
#    2 가
#    1 나
#    1 다
```

정렬을 거치지 않아도 재현된다. `uniq` 은 **인접한 줄이 같은지**만 보는데, 그 "같은지" 판정 자체가 무너진 것이다. `sort -u` 도 마찬가지로 `가` 하나만 남긴다.

```bash
LC_ALL=en_US.UTF-8 sort -u k.txt
# 가
```

## 왜 이런 일이

문자열 비교가 로케일의 **collation(정렬 대조) 규칙**을 타기 때문이다. POSIX 로케일 모델에서 `sort`·`uniq` 같은 도구는 바이트를 직접 비교하지 않고, 로케일이 정의한 대조 가중치(collation weight)로 비교한다. 두 문자열의 가중치가 같으면 "같은 값"이다.

macOS 가 쓰는 BSD 계열 도구 + 시스템 로케일 데이터에서는 `en_US.UTF-8` 의 대조 테이블이 한글 같은 비ASCII 문자를 제대로 다루지 못한다. 가중치를 뽑지 못하면 전부 동률이 되고, 동률이면 `uniq` 입장에서는 중복이다. 그래서 서로 다른 글자가 한 줄로 합쳐진다.

두 가지가 이 문제를 특히 고약하게 만든다.

- **리눅스에서는 재현되지 않는다.** GNU coreutils 는 같은 로케일에서 정상 동작하므로, CI(리눅스)에서 검증한 집계 스크립트가 로컬 맥에서만 조용히 틀린 숫자를 뱉는다. 반대 방향도 마찬가지다.
- **한글만의 문제가 아니다.** 일본어·중국어·키릴 문자·이모지 등 비ASCII 데이터 전반에 해당한다. 태그 집계, 카테고리 카운트, 로그의 유저 에이전트 통계 어디서나 터질 수 있다.

## 해결: 비교 단계의 로케일을 C 로 고정한다

`LC_ALL=C` 는 대조를 **바이트 단위 비교**로 떨어뜨린다. 사전순 한글 정렬은 포기하게 되지만, 동일성 판정은 정확해진다. 집계에서 필요한 건 후자다.

```bash
git log --pretty=format:'%an' | LC_ALL=C sort | LC_ALL=C uniq -c | LC_ALL=C sort -rn
```

주의할 점은 **파이프의 모든 단계에 붙여야 한다**는 것이다. `sort` 만 `C` 로 두고 `uniq` 를 놔두면, 정렬은 제대로 되어도 병합 단계에서 그대로 뭉개진다. 거꾸로도 마찬가지다 — `sort` 가 로케일 규칙으로 "동률"이라 판단한 줄들을 임의 순서로 흩어 놓으면, `C` 로케일 `uniq` 이 인접 비교를 해도 이미 늦다.

스크립트 전체에 걸겠다면 맨 위에서 한 번 export 하는 편이 안전하다.

```bash
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C   # 집계 정확도 우선
```

### 사람이 읽을 순서가 필요하면

역할을 나눈다. 집계는 `C` 로 정확하게 끝내고, 표시 순서는 마지막에 숫자 기준(`sort -rn`)으로 잡는다. 대부분의 리포트는 사전순이 아니라 빈도순이라 이걸로 충분하다.

사전순 한글 정렬 자체가 목적이라면 `LC_ALL=C` 가 답이 아니다. GNU coreutils 를 설치해 쓰는 편이 낫다.

```bash
brew install coreutils
gsort k.txt | guniq -c    # GNU 구현은 UTF-8 로케일에서 정상 동작
```

## 교차 검증을 습관으로

이 사례에서 오류를 잡아낸 건 `grep -c '<이름>'` 한 줄이었다. 파이프라인 결과를 **다른 경로로 한 번 더 확인**하는 습관이 없었다면 틀린 숫자를 그대로 보고했을 것이다.

집계 스크립트에 붙일 만한 최소한의 검산은 이런 것들이다.

```bash
# 1) 카운트 총합이 원본 줄 수와 일치하는가
LC_ALL=C sort f.txt | LC_ALL=C uniq -c | awk '{s+=$1} END {print "sum:", s}'
wc -l < f.txt

# 2) 고유값 개수가 두 방법에서 같은가
LC_ALL=C sort -u f.txt | wc -l
awk '!seen[$0]++' f.txt | wc -l    # awk 의 해시는 로케일 대조를 타지 않는다
```

`awk` 의 연관배열 키 비교는 바이트 기준이라 이 문제에서 자유롭다. 비ASCII 가 섞인 데이터를 셸로 집계할 때 `sort | uniq -c` 대신 `awk` 로 세는 것도 방법이다.

```bash
awk '{c[$0]++} END {for (k in c) print c[k], k}' f.txt | sort -rn
```

## 정리

- macOS 의 `sort`·`uniq` 은 `en_US.UTF-8` 에서 비ASCII 문자열을 동일하게 취급해 **조용히 병합**한다.
- 에러도, 경고도, 종료 코드 이상도 없다. 결과 숫자만 틀린다.
- 집계 파이프라인은 **모든 단계에** `LC_ALL=C` 를 걸어 바이트 비교로 고정한다.
- 사전순 정렬이 필요하면 GNU coreutils(`gsort`), 로케일 영향을 아예 피하려면 `awk` 해시 카운팅.
- 비ASCII 데이터 집계 결과는 다른 방법으로 한 번 검산한다.

## 참고

- [macOS `sort(1)` man page](https://keith.github.io/xcode-man-pages/sort.1.html)
- [POSIX: Locale 과 LC_COLLATE 정의](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap07.html)
- [GNU coreutils: sort invocation](https://www.gnu.org/software/coreutils/manual/html_node/sort-invocation.html)
