

기존 우리 회사의 공식 IDE는 intelliJ였다.
어플리케이션이 무거운 거 빼고는 크게 불편한 점이 없었고, 심지어 Git History 등은 훨씬 직관적이고 편한 UX를 가지고 있어서 좋았다.
하지만 cursor가 도입되면서 불편함이 생겼다.
회사 프로젝트 중에 하나가 vue + vuex 조합으로 구현되어 있는데, intelliJ는 vuex에 대한 definition을 딸깍ctrl(or cmd) + 마우스 좌클릭만으로 해결해줬다.
예를 들어 store.state.isOpen에서 "isOpen" 부분을 클릭하면 vuex createStore로 선언된 부분으로 이동시켜줬다.
근데 cursor와 vscode에는 해당 기능이 기본으로 탑재되어 있지 않았던 것이다.
pinia로 마이그레이션을 하면 이 기능이 제공될 것이라고 생각되지만, 개인적인 재미도 챙기고 vscode extension을 만드는 것이 마이그레이션보다 훨씬 간편하고 빠를 것이라해 vscode extension을 만들기 시작했다.
시작 당시만 해도 퇴근 후 30분 정도씩만 투자해서 일주일이면 뚝-딱 만들 줄 알았다.
참고로 vscode extension과 cursor extension 모두 배포했다.
시작하는 법은 공식 문서에 자세히 나와 있다.
node, npm yo, npm generator-code만 있으면 된다.
package.json이나 launch.json 등 기타 설정 파일 작업 후 npm run watch 명령어를 입력, F5 버튼(Run -> Start Debugging)을 띄워 테스트용 워크스페이스를 띄우면 프로젝트 세팅 완료이다.
여담이지만
처음에는 vscode API를 1도 몰랐기 때문에 AI에게 도움을 요청했지만, 학습 데이터가 적은지 단편적인 부분밖에 안 나왔다.
(store.commit("xx/yy") 이런 식으로 짜여 있는 코드에서 xx를 누르면 ~로, yy를 누르면 ~로 이동시키는 vscode extension을 만들고 싶어. 규칙은 어쩌구~ 라고 물었지만 정말 xx/yy에만 정확하게 동작하는 것을 만들어줬다)
그래서 그냥 만들어줘~는 못했고 QnA 봇 정도로만 사용했다.
이 아래부터는 각 버전별 어떤 고민 및 버그가 있었고, 어떻게 변경했는지에 대한 기록이다.
1) js, ts, vue에 대한 불완전한 구문 분석을 해낸다.
vscode에서 제공하는 기본 API만으로는 충분하지 않았다.
그래서 babel parser를 도입해서 직접 구문 분석을 했다.
babel parser로 직접 구문 분석을 하면 다음과 같은 장점이 있다.
import { createStore } from "vuex";
import { createStore as cs } from "vuex";
const store = useStore();
store.commit("zxcv");
const s = useStore();
s.commit("zxcv");
store로 통일되지 않아도 해당 scope에서는 어떠한 변수명으로 사용되는지 찾을 수 있고,async myAction({ commit }) {
// ...
}
async myAction({ commit: cm }) {
// ...
}
async myAction(context) {
// ...
}
2) state, getters, commit(mutations)에 대해 지원한다.
dispatch(actions)는 현재 진행 중인 프로젝트에서 거의 사용하지 않았기 때문에 후순위로 미뤘다.
3) 상대 경로와 절대 경로를 모두 지원한다.
나는 구조 리팩토링을 진행할 때 import 부분까지 수정되는 것을 신경쓰는 것을 방지하고자, 절대 경로를 사용하는 것을 선호하며 진행 중인 프로젝트도 절대 경로를 사용하고 있다.
{
"compilerOptions": {
"paths": {
"#store2": ["./src/store2/index"]
}
},
// ...
}
이처럼 단일 config 파일일 경우에도 지원되며,
{
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
],
// ...
}
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
// ...
}
여러 config 파일로 분산되어 있어도 path resolve가 가능하다.
4) nested module, nested object를 지원한다.
createStore({
state: {
a: { // 3번째 줄, 4번째 글자
b: { // 4번째 줄, 6번째 글자
c: "C" // 5번째 줄, 8번째 글자
}
}
},
// ...
});
위와 같이 선언되어 있고, store.state.a.b.c와 같이 사용한다면, a를 클릭했을 때 "3번째 줄, 4번째 글자" 위치로, b를 클릭했을 때 "4번째 줄, 6번째 글자" 위치로, c를 클릭했을 때 "5번째 줄, 8번째 글자" 위치로 이동했으면 좋겠다고 생각하여 해당 방향으로 개발했다.
또한
// module1.js
export const module1 = { // 0번째 줄, 23번째 글자
state: {
state1: "zz" // 2번째 줄, 4번째 글자
}
}
// index.js
createStore({
// ...
modules: {
module1,
}
})
store.state.module1.state1과 같이 사용한다면, module1을 클릭했을 때 "0번째 줄, 23번째 글자" 위치로, state1를 클릭했을 때 "42번째 줄, 4번째 글자" 위치로 이동했으면 좋겠다고 생각하여 해당 방향으로 개발했다.
위 1, 2, 3, 4 조건들을 모두 만족한,
state, getters, mutations, modules를 담은 중앙 symbol table이 필요했고, 완성한 symbol table은 다음과 같은 형식이다.
[
{
type: "modules",
name: "",
fileUri: "~/vuex-javascript-example/src/store/index.js"
position: {start: 0, end: 100},
pastNamespaces: [],
},
{
type: "modules",
name: "banner",
fileUri: "~/vuex-javascript-example/src/store/banner.js"
position: {start: 0, end: 100},
pastNamespaces: [
{
name: "",
isNamespaced: true,
}
],
},
{
type: "state",
name: "banner.isOpen",
fileUri: "~/vuex-javascript-example/src/store/banner.js"
position: {start: 100, end: 200},
},
{
type: "getters",
name: "banner.getIsOpen",
fileUri: "~/vuex-javascript-example/src/store/banner.js"
position: {start: 200, end: 300},
},
{
type: "mutations",
name: "banner/open",
fileUri: "~/vuex-javascript-example/src/store/banner.js"
position: {start: 300, end: 400},
}
]
1) vue SFC 파일 내 태그 선언 순서에 상관 없이 vue 파일 구문 분석이 가능하다.
// blank
<script>
// ...
</script>
<template>
<!-- ... -->
</template>
<script>
// ...
</script>
1.0.0 버전까지는 <script> 태그가 0번째 줄에 시작되어야지만 문제 없이 분석이 가능했다.
하지만 SFC에는 <script> 태그 외에도 <template> 태그, <style> 태그가 존재하기 때문에 <script> 태그가 어느 위치에 존재해도 문제 없이 구문 분석이 가능해야 했다.
2) 파일 수정 시 AST(Abstract Syntax Tree) cache에서 제거한다.
특정 파일에 접근할 때 매번 구문 분석을 하는 것은 비효율적이므로, 1.0.0에서 AST cache를 추가했었다.
하지만 파일이 변경되었음에도 AST cache에서 사라지지 않아 버그가 발생했다.
vscode API로부터 파일 변경 시 이벤트를 받아, 변경된 파일은 AST cache에서 사라지게끔 처리했다.
1) cursor marketplace에서도 설치가 가능하다.
vscode marketplace에 올리면 cursor marketplace에도 자동으로 업로드되는 줄 알았는데, 아니었다.
cursor marketplace는 open vsx registry라고 별도로 존재했다.
cursor에도 배포하면서 vscode marketplace에서와 open vsx registry에서의 버전을 맞추기 위해 업그레이드를 진행했다.
1) 파이프라인을 자동화했다.
vscode marketplace 팀으로부터 메일이 왔다.
vscode extension을 빌드하면 vsix 파일이 나오는데, 그 파일에 내 토큰이 들어있다는 것이다. ㄴㅇㄱ

gitignore된 파일에 token을 써놓고 1.0.2 버전까지는 로컬에서 빌드해서 업로드했지만, vsix 빌드할 때 src 폴더 내부만 참조하는 게 아니라 프로젝트에 있는 파일을 전부 담나보다.
똑같은 실수를 반복하지 않기 위해(그리고 marketplace에서 내려가지 않기 위해...) github actions를 만들고 파이프라인에서 token을 입력받도록 수정했다.
cf) 기존 토큰은 제거하고 새로 발급 받았습니다. 1.0.2 버전의 extension을 설치해도 토큰 이용 못합니다...
2) dispatch(actions)도 지원한다.
다음과 같이 dispatch는 또다른 dispatch 혹은 commit을 호출할 수 있다.
{
actions: {
async increment({ dispatch, commit }, payload) {
return new Promise((resolve) => {
setTimeout(() => {
dispatch("incrementBy", 1);
commit("incrementBy", payload)
resolve();
}, 500);
});
},
},
// ...
}
위와 같이 선언된 부분에서 "incrementBy"를 클릭해도 선언한 위치로 이동할 수 있도록 기능을 추가했다.
1) vscode engine 지원 버전을 낮췄다(^1.102.0 -> ^1.99.0).
2025년 8월 기준으로 1.104 버전의 vscode가 배포됐다.
하지만 (현재) 최신 cursor 기준으로 vscode 엔진은 1.99가 가장 높은 버전이다.

그래서 2.0.0 버전 전까지는 cursor에서 extension을 사용할 수 없었다. ㅠ
vscode extension api guide를 찾아봐서 1.99 버전에도 내가 사용 중인 API를 모두 제공하는지 찾아보려고 했는데... Node의 공식 문서처럼 버전 별로 제공하는 API인지 아닌지 구분을 따로 해서 보여주지 않는다.
그래서 그냥 냅다 낮췄다. 일단 사용하고 버그가 발생하면 그때 고치자!
근데 다행히 문제 없이 동작한다.
2) 테스트 코드를 추가했다.
기능이 추가되고, 버그를 수정하고, 버전을 업그레이드할 때마다 매번 수동 테스트를 진행했다.
이 시간이 쌓이니까 테스트하는 시간에 개발을 했으면 더 빨랐을 걸... 이라는 생각이 들었다.
일주일 안에 후딱 만들고 때려칠 프로젝트라고 생각해서 테스트 코드를 안 짰었는데, 이제는 확실히 필요해졌다.
테스트에서 실행할 파일, 테스트할 워크스페이스는 .vscode-test.mjs 또는 .vscode-test.cjs 또는 .vscode-test.js 파일로 선언하면 설정이 간단한다.
import { defineConfig } from "@vscode/test-cli";
export default defineConfig([
{
label: "js basic defintion integration tests",
files: "out/test/defintion/js-basic.test.js",
workspaceFolder: "./fixtures/definition/js/basic",
mocha: {
timeout: 10000, // 10s
slow: 2000, // 1s
},
},
// ...
]);
그런데 테스트 코드를 추가하면서 한 가지 문제가 있었다.
extension이 로드가 되기 전에 테스트 코드가 실행된다는 점이었다.
그래서 extension을 로드할 때까지 100ms를 대기하는 코드를 추가했다
import * as vscode from "vscode";
export async function waitForLoadingExtension() {
const extension = vscode.extensions.getExtension(
"qjsrodksro.vuex-reference-helper"
) as vscode.Extension<any>;
if (!extension.isActive) {
await extension.activate();
}
// Wait a bit more to ensure symbol table is fully built
await new Promise((resolve) => setTimeout(resolve, 100));
}
cf) 테스트가 추가될 때마다 100ms 지연이 계속 추가되고 있습니다. 100ms 지연시키는 것이 최선의 방법이 아닌 거 같은데, 누가 아는 사람이 댓글 부탁드립니다.
1) optional chaining과 non-null assertion operation도 지원한다.
vuex reference helper에서 구현한 DefinitionProvider는 특정 단어를 클릭하면 그 앞에 state, dispatch, commit, getters이 있을 때까지 .이나 [" 등을 통해 상위 모듈 스코프를 추적한다.
(store.state.module1.state1에서 state1 클릭 시 module1.state1을 추출하고, store.commit["module1/mutation1"]에서 mutation1 클릭 시 module1/mutation1을 추출한다)
다만, 2.0.0 버전까지 간과하고 있던 것은, js/ts는 ?. 처럼 optional chaining operation이 있다는 것과, ts는 !.처럼 non-null assertion operation이 있다는 것이었고 이러한 operation이 있다면 의도한 대로 단어 추출이 불가능했다.
const store = useStore();
console.log(store?.state?.myBanner?.isOpen);
const store = useStore();
console.log(store!.state?.myBanner!.isOpen);
2.0.1부터는 이러한 operation이 있어도 상관 없이 정상적으로 동작할 수 있도록 수정했다.
1) 파일에 변화가 생기면 symbol table을 다시 만든다.
코딩하는 중간에 store파일은 언제든지 변경될 수 있다.
이를 실시간으로 반영하기 위해서 vuex를 사용하는 파일에 변경(create, delete, rename, edit 등)이 발생하면 최신 데이터로 definition 위치 추적이 가능할 수 있는 기능이 필요했다.
vuex는 다음과 같이 중앙 스토어를 사용하는 부분을 명시해야 한다(가이드에 명시된 방식).
// store/index.js
export default createStore({
// ...
})
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import { store } from "./store"; // [TEST] normally execute when store is a named export
const app = createApp(App);
app.use(store);
app.mount("#app");
이렇게 중앙 스토어를 사용하는 파일에 변경 사항이 생기면 symbol table을 갱신한다.
IDE의 tab을 통한 자동 완성은 생산성을 엄청나게 높여준다.
vscode intellisense를 활용하여 자동 완성까지 지원하면 더 완성도 있는 extension이 될 거라 생각해서 추가했다.
import { createStore } from "vuex";
export const store = createStore({
state: {
count: 0,
appDetail: {
scrollPosition: 100,
size: {
width: 0,
height: 0,
},
},
abc: "abc",
},
// ...
});
위와 같이 store가 선언된 상태에서 IDE에 아래와 같이 입력하면
const st = useStore();
st.state.
st.state.countst.state.appDetailst.state.scrollPositionst.state.sizest.state.size.widthst.state.size.heightst.state.abc이러한 것에 대한 자동 완성을 추천해준다.
getters, mutations, actions에도 모두 동작한다.
최종 버전은 2.2.0이 아니라 2.2.1이다.
2.2.0에 demo video를 넣는 것을 깜빡해서... README.md만 수정했을 뿐 나머지는 달라지지 않았다.
현재로서는 2.2.1 기준으로 추가 기능 개발은 진행하지 않을 계획이다.
cf) 많은 분들이 react로 프로젝트를 진행 중일 것이고, vue로 진행한다고 하더라도 pinia를 사용할 것이라고 생각합니다. 혹시라도 vuex를 사용하고 있는 레거시 프로젝트가 있다면, 사용하고 피드백 한 번씩만 부탁드리면 감사하겠습니다.
( _ _ )