파일 이름의 대소문자만 바꿨는데 CI에서만 모듈을 못 찾은 이유
문제 발생
컴포넌트 파일 하나만 소문자로 시작하는 게 눈에 걸려서 이름을 맞췄습니다. 에디터에서 userCard.tsx를 UserCard.tsx로 바꾸고 import 경로도 같이 고쳤습니다.
import { UserCard } from "./components/UserCard";제 맥에서는 타입 검사도 빌드도 통과했습니다. 그런데 PR을 올리자마자 CI가 실패했습니다.
src/App.tsx(1,26): error TS2307: Cannot find module './components/UserCard' or its corresponding type declarations.로컬에서 다시 돌려 봐도 멀쩡했습니다. 한참 뒤에야 git status를 제대로 들여다봤는데, 바뀐 파일로 잡힌 건 import를 고친 App.tsx 하나뿐이었습니다. 이름을 바꾼 파일은 목록에 없었고, GitHub의 파일 목록에도 여전히 userCard.tsx로 올라가 있었습니다.
원인 분석
맥과 윈도우는 기본적으로 파일 이름의 대소문자를 구분하지 않습니다. gitfaq에 따르면 Git은 파일 이름을 바이트열 그대로 저장하고 대소문자를 합치지 않지만, Windows와 macOS는 기본으로 대소문자만 다른 이름을 같은 이름으로 취급합니다. 제 맥에서는 ./components/UserCard로 찾든 ./components/usercard로 찾든 같은 파일이 열립니다. 리눅스에서 도는 CI는 글자 그대로 찾습니다.
Git은 이런 파일 시스템 위에서 대소문자 차이를 일부러 넘깁니다. core.ignoreCase 문서는 이 값을 APFS, HFS+, FAT, NTFS처럼 대소문자를 구분하지 않는 파일 시스템에서 Git이 잘 동작하도록 여러 우회 동작을 켜는 내부 변수라고 설명합니다. 문서가 드는 예가 이번 일과 똑같습니다. 디렉터리를 읽었더니 makefile이 나왔는데 Git이 알고 있는 이름이 Makefile이면, Git은 같은 파일로 보고 계속 Makefile로 기억합니다. 이 값은 git clone이나 git init이 저장소를 만들 때 파일 시스템을 확인해서 알아서 true로 넣어 둡니다.
그러니 디스크의 이름은 UserCard.tsx가 됐지만 Git이 기억하는 이름은 userCard.tsx 그대로였습니다. 커밋에도 옛 이름으로 들어갔고, 리눅스에서 체크아웃하면 userCard.tsx가 생깁니다. 코드는 ./components/UserCard를 찾으니 없는 게 당연했습니다.
해결 방안
- Git이 기억하는 이름부터 확인합니다. 탐색기나 에디터에 보이는 이름이 아니라 이 출력이 저장소에 올라가는 이름입니다. 제 경우엔
src/components/userCard.tsx가 그대로 나왔습니다.
git ls-files | grep -i usercard- 이름은
git mv로 바꿉니다.
git mv src/components/userCard.tsx src/components/UserCard.tsx
git statusgit status에 renamed: src/components/userCard.tsx -> src/components/UserCard.tsx가 보이면 된 겁니다. 예전 Git은 이 명령을 "destination already exists"로 거부해서 --force가 필요했는데, 대소문자만 다른 경우는 따로 알아보고 받아 주도록 바뀌었습니다. 폴더 이름이거나 그래도 거부당하면 임시 이름을 한 번 거칩니다.
git mv src/components src/components_tmp
git mv src/components_tmp src/Components-
core.ignorecase를false로 바꾸지 않습니다. 검색하면 제일 먼저 나오는 처방인데, 문서는 Git이 이 변수가 운영체제와 파일 시스템에 맞게 설정돼 있다고 믿고 동작하며 값을 바꾸면 예상하지 못한 동작이 생길 수 있다고 적어 둡니다. 이 값을 끄고git add를 하면UserCard.tsx가 새 파일로 추가될 뿐 옛 이름은 인덱스에 그대로 남아서, 저장소에 두 이름이 다 올라가기 쉽습니다. -
두 이름이 이미 다 올라갔다면 하나를 인덱스에서만 지웁니다. gitfaq의 "Why do I have a file that's always modified?" 항목이 이 상황입니다. 파일 시스템은 둘 중 하나만 담을 수 있어서, 맥이나 윈도우에서는 손대지 않은 파일이 계속 수정된 것으로 보입니다. 문서가 알려 주는 순서는 이렇습니다.
git rm --cached src/components/userCard.tsx
git commit -m "Remove files conflicting in case"
git checkout .마지막 git checkout .은 작업 트리의 변경을 되돌리니, 문서 말대로 다른 변경이 없는 깨끗한 상태에서 합니다.
- CI에서 대소문자만 다른 경로를 걸러 냅니다. gitfaq는 이름 규칙을 정해 두고 pre-receive 훅이나 CI에서 검사할 수 있다고 적어 둡니다. 저는 이 몇 줄을 CI 앞쪽에 넣었습니다.
dups=$(git ls-files | sort -f | uniq -di)
if [ -n "$dups" ]; then
echo "대소문자만 다른 경로가 있습니다:"
echo "$dups"
exit 1
fi-
import 쪽 실수는 TypeScript가 잡게 둡니다. 반대 방향, 그러니까 파일은
UserCard.tsx인데 import를./usercard로 적은 경우도 맥에서는 통과합니다. 이건forceConsistentCasingInFileNames가 잡습니다. TypeScript 문서에 따르면 이 옵션이 켜져 있으면 디스크의 대소문자와 다르게 파일을 불러오려 할 때 오류를 냅니다. TypeScript 5.0부터는 기본값이true이니,tsconfig.json에서 누가false로 꺼 두지 않았는지만 확인하면 됩니다. -
타입 검사와 빌드는 리눅스에서 한 번은 돌립니다. 이번 일을 잡아 준 건 결국 리눅스에서 도는 CI였습니다. 팀 전원이 맥을 쓴다면 이런 차이는 로컬에서 절대 드러나지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.