개요

기간: 2025. 07. ~ 2026. 04. (이후 오픈소스 유지보수)
사용 기술: Kotlin, IntelliJ Platform, Grammar Kit, JFlex
결과: JetBrains Marketplace 배포, 누적 다운로드 15,000회 이상
- JetBrains Marketplace: https://plugins.jetbrains.com/plugin/27949-gdshader
Godot 엔진의 셰이딩 언어인 GDShader를 IntelliJ 기반 IDE에서 쉽게 편집할 수 있도록 지원하는 플러그인입니다. 문법 토큰화와 파싱부터 시작하여 코드 하이라이팅, 자동완성, 포매팅, 심볼 찾기, 리팩터링 등의 IDE 기능을 구현했습니다. 개발에는 IntelliJ Platform SDK와 Kotlin이 사용됐습니다.
프로젝트 목표
GDShader는 Godot 엔진에서 사용되는 셰이딩 언어입니다. Rider IDE에서 스크립트 언어와는 달리 GDShader가 지원되지 않기 때문에 셰이더 작업이 있는 경우 일관적인 개발 환경을 경험하기 어려웠습니다. Rider에서 GDShader를 지원한다면 엔진 자체의 에디터에 비해서 하이라이팅, 자동완성, 포매팅 등의 더 많은 에디터 기능을 적용할 수 있을 것이라고 판단했습니다.
IntelliJ 기반 IDE는 커스텀 언어를 지원하는 SDK를 제공하고 있습니다. SDK를 활용하여 GDShader를 하나의 커스텀 언어로서 IDE의 개발 환경에 통합하는 것을 목표로 했습니다.
기능
코드를 파싱하여 생성된 PSI 트리를 기반으로 아래의 기능들이 구현됐습니다.
- 문법 하이라이팅
- 코드 자동완성
- 코드 포매팅
- 심볼의 정의 위치 이동 / 사용 위치 찾기
- 심볼 이름 변경
- 문법, 의미적 오류 감지
- 빌트인 함수, 변수 등의 문서화 설명 제공
- 색상 선택기
구조
구현
Lexer/Parser
GDShader 코드를 해석하는 첫번째 단계로, Lexer를 거쳐 토큰 단위로 분리하는 과정, Parser를 거쳐 PSI 트리로 변환하는 과정을 거쳐야 합니다.
Lexer와 Parser 생성에는 JetBrains의 Grammar-Kit을 사용했습니다. GDShader의 토큰은 JFlex 문법으로 정의해 Lexer를 생성했고, 각 토큰을 기반으로 GDShader 문법을 BNF로 작성해 Parser를 생성했습니다.
이 과정에서 목표는 단순히 코드가 문법적으로 유효한지만 판별하는 것이 아니라, 이후 자동완성, 참조 탐색, 리팩터링, 포매팅 등의 기능이 활용할 수 있는 이해할 수 있는 PSI 구조를 만드는 것이었습니다. 예를 들어 변수 선언, 함수 선언, 표현식, 타입 정보가 PSI 트리에서 의미 있는 단위로 구분되어야 이후 기능을 안정적으로 구현할 수 있습니다.
또한 IDE에서는 사용자가 코드를 작성하는 도중에는 일시적으로 문법적으로 완성되지 않은 상태가 자주 발생합니다. 이때 Parser가 지나치게 엄격하게 동작하면 PSI 트리가 깨지고, 자동완성이나 하이라이팅 같은 기능도 함께 동작하지 않을 수 있습니다. 따라서 Parser는 가능한 한 편집 중인 불완전한 코드도 PSI로 표현할 수 있도록 구성하고, 문법적·의미적 오류는 별도의 에디터 기능에서 검사하도록 분리했습니다.
PSI 기반 IDE 기능 구현
Lexer와 Parser를 거쳐 생성된 PSI 트리는 이후 IDE 기능을 구현하는 기반이 됩니다. GDShader 코드는 변수 선언, 함수 선언, 구조체 선언, 표현식 등의 단위로 파싱되어 트리를 구성하고, 플러그인은 이 구조를 기반으로 자동완성, 정의 위치 이동, 사용 위치 찾기, 이름 변경 리팩터링 등의 기능을 제공합니다.
예를 들어 표현식에 등장하는 심볼이 어떤 선언을 참조하는지 파악하기 위해서는 현재 위치의 PSI context를 기준으로 접근 가능한 선언을 탐색해야 합니다. 지역 변수, 함수 파라미터, 전역 변수, uniform, varying, 함수 선언은 각각 사용 가능한 범위가 다르기 때문에, scope 규칙에 따라 적절한 정의 위치를 찾아 reference로 연결했습니다.
이렇게 구성한 참조 해석 로직은 Go to Definition, Find Usages, Rename Refactoring처럼 심볼 간 연결 관계가 필요한 기능에서 공통으로 사용됩니다. 또한 코드 하이라이팅, 포매팅, 자동완성도 PSI 트리의 구조와 현재 작성 위치의 context를 기반으로 동작하도록 구현했습니다.
타입 추론과 함수 호출 검사
파싱만으로는 함수 호출이 올바른지, 표현식이 어떤 타입을 갖는지 판단할 수 없습니다. 이를 위해서 플러그인 내부에 GDShader의 타입 시스템을 표현하는 도메인 모델을 구현했습니다. 기본 타입, 벡터 타입, 행렬 타입, 샘플러 타입, 배열 타입 등을 모델링하고, 변수 선언이나 함수 선언을 이 모델로 변환해 사용했습니다. 이때, 모든 타입을 enum이나 문자열로 처리하지 않고 지원 동작을 인터페이스로 분리했습니다. 이 덕분에 기능의 구현을 수정하지 않고도 새로운 타입을 추가할 수 있는 등의 이점이 있었습니다.
빌트인 함수, 변수 등도 이 타입 모델을 기반으로 저장되어 있습니다. 사용자가 작성한 코드가 이 타입 모델로 변환되기 때문에 함수 호출에 전달된 파라미터의 타입이 유효한지, 표현식의 타입이 유효한지 등을 검사할 수 있습니다. 또한 자동완성 등에도 키워드의 타입 정보가 표시되도록 하기에도 용이했습니다.
에디터 기능
언어 지원 플러그인은 코드 분석 기능뿐만 아니라 코드를 작성할 때의 편집 경험도 중요하다고 생각했습니다. 따라서 코드 포매팅, 코드 스타일 설정, 코드 접기, Structure View, 그리고 색상 선택기 등의 기능을 함께 구현했습니다.
코드 포매팅과 코드 스타일도 구문의 문맥을 파악할 수 있도록 PSI 트리를 기반으로 구현했습니다. 사용자가 에디터의 설정에서 많은 부분을 개인적으로 선택할 수 있도록 옵션을 노출했습니다.
셰이더 코드에는
vec3 또는 vec4 타입의 색상 값이 들어가는 경우가 많습니다. 따라서 이 색상을 에디터에서 편하게 확인하고 조절할 수 있도록 색상 선택기를 구현했습니다. 플러그인이 타입과 변수명을 통해서 색상 값으로 추정되는 위치를 식별하고, 이러한 값을 색상 선택기로 쉽게 변경할 수 있습니다.테스트
플러그인의 기능들은 대부분 Lexer와 Parser를 거쳐 생성된 PSI 트리를 기반으로 동작합니다. 따라서 새로운 문법을 추가하거나 기존 문법을 수정하면 PSI 트리에 의존하는 기존의 기능들이 깨질 가능성이 있었습니다.
이를 확인하기 위해서 Parser, 자동완성, 참조 해석, 포매팅 등에 대한 테스트를 작성했습니다. 기능을 추가하면서 Lexer/Parser를 수정할 때 기존 기능이 유지되는지 빠르게 파악할 수 있다는 점에서 큰 도움이 됐습니다.
배포/유지보수
플러그인은 JetBrains Marketplace의 심사를 거쳐 배포했습니다. 개인적으로만 사용하는 도구가 아니라 실제 사용자가 설치하고 사용할 수 있는 형태로 공개했다는 점에서 의미있는 경험이었습니다.
배포 이후에는 GitHub를 통해 프로젝트를 유지보수했습니다. 기여 가이드를 작성해 빌드와 기여 절차를 문서화했고, Pull Request가 생성되면 GitHub Actions로 테스트가 자동 실행되도록 구성했습니다. 이를 통해 GitHub 내에서 외부 기여를 테스트하기에 편리했습니다.
또한 Issue와 Pull Request를 통해 들어온 피드백과 외부 기여를 처리하면서 문서화, 리뷰, 커뮤니케이션도 중요한 요소라는 점을 경험했습니다.
현재 상태와 개선 방향
현재 이 플러그인은 JetBrains Marketplace에 배포되어 누적 다운로드 13,000회 이상으로, 실제 사용자들이 사용하고 있는 상태입니다. 배포 이후에도 리뷰와 GitHub를 통해서 피드백을 받고 있습니다.
많은 에디터 기능들이 제공되고 있지만, 일부 edge case나 개선할 부분도 남아 있습니다. Godot의 버전에 따른 문법 차이 대응, 전처리기 지원 확대, 문법 검사 정확도 개선을 중심으로 확장할 계획입니다.