Jaeyoon.dev

회고

Vite 프로젝트의 설치 오류를 따라가며 오픈소스에 기여하기

Vite 프로젝트의 설치 오류를 따라가며 오픈소스에 기여하기 게시글 대표 이미지

tl;dr

create-vite@9.3.3으로 React + TypeScript + ESLint 프로젝트를 만들다가 의존성 설치 오류를 만났다. 코드를 살펴보니, 호환되는 TypeScript 버전을 지정해도 이전 값으로 덮어쓰는 처리 순서가 원인이었다. 순서를 바로잡고 회귀 테스트를 추가해 Vite 저장소에 PR을 보냈으며, 수정은 create-vite@9.3.4에 반영됐다. 사용 중 발견한 오류를 지나치지 않고 원인을 확인한 것이 오픈소스 기여로 이어진 경험이었다.

시작하며

Vite로 새 프로젝트를 만들고 의존성을 설치하는데 에러가 발생했다. React와 TypeScript를 선택하고 ESLint를 추가했을 뿐인데, 코드를 작성하기도 전에 설치가 막혔다.

처음에는 신기했다. React 프로젝트를 시작할 때 자연스럽게 선택할 만큼 익숙하고, 많은 사람이 사용하는 도구에서 설치부터 막혔기 때문이다. “나만 겪는 문제인가? 관련 이슈나 PR이 올라와 있지는 않을까?”라는 생각이 들었다. 의존성 버전을 직접 바꿔서 진행할 수도 있었지만, 어떤 이유로 이런 에러가 발생했는지 확인해 보고 싶었다.

그렇게 오류 로그와 생성기 코드를 살펴봤고, 원인을 찾은 뒤 수정과 테스트를 담아 Vite 저장소에 PR을 보냈다. 이 글은 그 과정에서 무엇을 확인했고, 어떻게 문제를 해결했는지 정리한 기록이다.

문제 상황: 생성된 프로젝트의 의존성을 설치할 수 없었다

문제가 발생한 버전은 프로젝트 생성기인 create-vite@9.3.3이었다. React + TypeScript 템플릿에서 ESLint를 선택하면 다음 명령으로 설치 오류를 재현할 수 있었다.

npm create vite@9.3.3 repro -- --template react-ts --eslint --no-interactive --no-immediate
cd repro
npm install

npm install은 ERESOLVE 에러와 함께 중단됐다. 오류 로그에서 확인한 충돌은 다음과 같았다.

Found: typescript@7.0.2
  dev typescript@"~7.0.2" from the root project

Could not resolve dependency:
peer typescript@">=4.8.4 <6.1.0" from typescript-eslint@8.72.1
  dev typescript-eslint@"^8.72.0" from the root project

생성된 package.json에는 TypeScript ~7.0.2가 지정돼 있었다. 반면 함께 설치되는 typescript-eslint는 TypeScript 6.1.0 미만을 요구했다. 생성기가 만든 두 의존성의 버전 범위가 맞지 않아 설치할 수 없는 상태였다.

원인 분석: 호환 버전을 지정하는 코드는 이미 있었다

Codex와 함께 로그와 create-vite 코드를 살펴봤다. 로그로는 어떤 버전이 충돌하는지 알 수 있었지만, 생성기가 왜 호환되지 않는 버전을 기록했는지는 코드를 확인해야 했다.

ESLint 설정을 담당하는 setupEslint 함수에는 TypeScript 프로젝트의 버전을 ~6.0.3으로 변경하는 코드가 이미 있었다. ESLint를 선택한 경우에 사용할 호환 버전이 정해져 있었던 것이다. 그런데 최종 package.json에는 여전히 TypeScript 7이 남았다.

버전을 바꾸는 코드가 실행된 뒤 어떤 일이 일어나는지 따라가 보니, 문제는 처리 순서에 있었다. 기존 코드는 devDependencies를 Object.entries()로 배열로 만든 다음 원본 객체의 TypeScript 버전을 바꿨다. 마지막에는 앞서 만들어 둔 배열로 객체를 다시 구성했다.

ESLint 의존성을 추가하고 정렬하는 부분을 생략하면 다음과 같은 흐름이다.

const asObject = {
  devDependencies: { typescript: '~7.0.2' },
};

// 변경 전 버전이 배열에 담긴다.
const devDepsEntries = Object.entries(asObject.devDependencies);

asObject.devDependencies.typescript = '~6.0.3';

// 앞서 만든 배열로 객체를 다시 구성하면서 이전 버전이 복원된다.
asObject.devDependencies = Object.fromEntries(devDepsEntries);

console.log(asObject.devDependencies.typescript); // "~7.0.2"

Object.entries()로 만든 배열이 원본 객체의 속성 변경을 계속 따라가는 것은 아니다. 이 경우 배열에 담긴 버전은 문자열이므로, 원본 객체에 새 문자열을 할당해도 배열에는 이전 문자열이 남아 있다. 이후 Object.fromEntries()로 객체를 다시 만들면서 변경했던 버전이 덮어써졌다.

즉, 호환 버전을 지정하는 처리는 있었지만, 그 값이 최종 파일에 반영되지 않는 문제였다.

왜 객체를 배열로 바꿨을까?

처음부터 객체로 관리하던 의존성을 왜 배열로 바꿨다가 다시 객체로 만들었을까?

create-vite@9.3.3의 코드를 보면 배열로 변환한 뒤 filter()로 Oxlint를 제거하고, push()로 ESLint 관련 의존성을 추가했다. 마지막에는 sort()로 정렬하고 다시 객체로 만들어 저장했다. 배열은 의존성 목록을 가공하고 정렬하기 위한 중간 형태였던 것이다.

이 과정에서 의존성 추가는 배열에, TypeScript 버전 변경은 원본 객체에 적용되고 있었다. 최종 파일에 기록할 의존성 객체는 배열을 바탕으로 만들어졌기 때문에 원본 객체에만 반영한 변경은 사라졌다. 값을 변경하는 것에서 끝내지 않고, 그 변경이 최종 결과까지 반영되는지도 확인해야 했다.

수정: 버전을 변경한 뒤 배열을 만들도록 순서를 바꿨다

원인을 확인한 뒤에는 TypeScript 버전 변경을 Object.entries() 호출 앞으로 옮겼다. 같은 예제에 수정을 적용하면 다음과 같다.

const asObject = {
  devDependencies: { typescript: '~7.0.2' },
};

asObject.devDependencies.typescript = '~6.0.3';

// 호환 버전으로 변경한 뒤 배열을 만든다.
const devDepsEntries = Object.entries(asObject.devDependencies);
asObject.devDependencies = Object.fromEntries(devDepsEntries);

console.log(asObject.devDependencies.typescript); // "~6.0.3"

실제 수정도 if (isTs) 안의 버전 변경을 배열 생성보다 먼저 실행하도록 옮기는 작업이었다. 기존에 정해 둔 TypeScript 6 호환 처리가 생성된 프로젝트에 그대로 반영되도록 했다.

수정한 코드의 양은 적었다. 다만 원인을 이해하려면 버전을 대입하는 한 줄뿐 아니라, 그 값이 배열로 옮겨지고 다시 객체가 되어 파일에 기록되는 흐름까지 확인해야 했다.

검증: 같은 문제가 다시 생기면 테스트가 실패하도록

수정과 함께 회귀 테스트를 추가했다. ESLint를 선택한 TypeScript 프로젝트에 호환 버전이 들어가는지, 다른 선택지의 의존성은 기존대로 유지되는지 확인했다.

프로젝트 생성 조건확인한 내용
react-ts + ESLintTypeScript가 ~6.0.3이고, typescript-eslint가 있으며, Oxlint는 없음
react-compiler-ts + ESLint같은 호환성 조건을 만족함
기본 Oxlint를 사용하는 react-ts템플릿의 TypeScript 버전을 유지함
JavaScript React + ESLinttypescript와 typescript-eslint가 추가되지 않음

추가한 TypeScript + ESLint 테스트는 수정 전 코드에서 실패했고, 수정 후에는 통과했다. 당시 create-vite CLI 테스트 25개도 모두 통과했다. 이 확인을 통해 테스트가 이번 버그를 실제로 잡아내는지 살펴볼 수 있었다.

생성된 package.json의 값만 확인하는 데서 끝내지 않고, 수정한 CLI로 React + TypeScript + ESLint 프로젝트를 만들어 처음 오류가 났던 설치 과정부터 다시 확인했다.

npm install
npm run build
npm run lint

세 명령이 모두 통과했다. create-vite 자체의 빌드와 타입 검사, 변경 파일의 포맷과 린트 검사도 통과했고, 검증 결과를 PR 본문에 정리했다.

PR과 배포: 사용 중 만난 문제를 수정으로 돌려보내기

재현 명령과 원인, 수정 내용, 검증 결과를 정리해 fix(create-vite): keep TypeScript 6 when using ESLint · #23931을 작성했다. PR은 2026년 10월 30일 Vite 메인테이너인 sapphi-red의 승인을 받아 머지됐다.

수정은 11월 5일 배포된 create-vite@9.3.4에 포함됐다. 이 버전에서는 ESLint를 선택한 TypeScript 프로젝트에 호환 버전이 반영되어, 이번에 겪었던 의존성 충돌 없이 설치할 수 있다.

이번 작업에서는 문제 조사부터 패치와 테스트 작성, 영어 PR 설명을 준비하는 과정까지 Codex의 도움을 받았다. 검증 명령도 Codex가 실행했다. 나는 버전 변경이 배열 생성보다 먼저 이뤄지는지 코드를 확인하고, 수정 전후의 테스트 결과를 검토했다. 이 협업 방식은 PR 본문에도 밝혀 두었다.

돌아보며

이번에는 설치 오류를 만났을 때 원인을 조금 더 따라가 본 것이 오픈소스 기여로 이어졌다. 내 프로젝트의 의존성 버전만 바꾸면 당장 개발을 이어갈 수 있었겠지만, 생성기에서 문제가 생긴 이유를 찾아 고치면서 이후 같은 선택을 하는 사용자들이 겪을 오류도 막을 수 있었다. 수정된 버전으로 프로젝트를 만드는 사람들은 저마다 같은 오류의 원인을 찾고, 일일이 의존성 버전을 바꾸는 데 시간을 쓰지 않아도 됐다.

이번 경험을 통해 눈앞에 드러난 오류에서 한 단계 더 들어가, 그 오류를 만든 원인을 살펴보는 일의 중요성을 느꼈다. 설치를 막은 것은 의존성 버전 충돌이었지만, 실제로 고쳐야 할 곳은 생성기의 처리 순서였다. 문제의 근본을 이해하고 나니 어디를 수정해야 하는지가 분명해졌다. 원인을 깊이 들여다보는 데 들이는 시간이 해결의 방향을 잡아 주고, 같은 도구를 쓰는 사람들의 반복 작업을 줄여 생산성을 높이는 데에도 도움이 될 수 있다는 것을 배웠다.

앞으로도 개발하다 예상과 다른 동작을 만나면, 재현할 수 있는 조건을 정리하고 왜 그런 결과가 나왔는지 한 단계 더 깊이 확인하는 습관을 기르고 싶다. 원인을 찾았다면 널리 쓰이는 도구라는 이유로 주저하지 않고, 이슈로 제보하거나 수정안을 제안하는 행동으로 이어가려 한다. 평소 쓰던 도구에서 발견한 작은 문제도 기여의 출발점이 될 수 있다는 것을 알게 된 만큼, 앞으로는 더 적극적으로 참여하고 싶다.