Spotify 다음으로 만들 상태 콘텐츠는 바로 visual studio code 상태 카드, 정확힌 Discord presence의 coding 프로필과 같이 현재 개발 상태를 보여주는 것이 목적이다.
단순히 작업 경로, 커서 위치, 레포지토리 uri만을 얻으면 된다.
Discord rich presence 확장은 직접 데이터를 수집하여 연동한 디스코드 계정으로 전달한다. 처음엔 vscode RESTful API라도 있지 않을까 싶었지만 구조적으로 vscode 유저의 특정성이 확립되지 않아 표준 API일리가 없음을 깨달았다.
결국 이 문제를 해결할려면 직접 vscode 확장을 만들어야 한다. *긴 한숨...*
visual studio code의 확장 프로그램을 만드는 일은 생각보다 매우 간단했다.
https://code.visualstudio.com/api/get-started/your-first-extension
알찬 가이드라인 뿐만이 아니라 생성 툴킷까지 모두 제공해주고 있다! 개발자는 그저 몇가지 입력만 해주면 될 뿐이다. 확장계의 CRA를 만난 느낌이다.
처음 프로젝트가 생성되면 package.json의 activation
에는 오직 명령어 이벤트만 있어서 꼭 명령어를 실행해야만 activate되는 번거로움이 있었다.
onStartupFinished
이벤트는 vscode 시작이 모두 끝나면 activate되는 이벤트로써 에디터가 실행될 때 확장이 활성화되어야 하는 지금에 적절한 이벤트다.
vscode 확장은 통상적으로 vscode 모듈에서 에디터의 모든 것을 제어하고 관찰할 수 있다.
vscode.window.onDidChangeTextEditorSelection
이벤트 리스너 함수는 텍스트 편집기의 선택, 즉 커서 위치가 바뀔 때 호출된다. 커서 위치를 얻어야 하니 가장 적절한 리스너가 아닐 수가 없다.
이벤트는 textEditor
, 커서가 있는 에디터와 selections
, 선택값 데이터와 kind
, 선택 종류로 총 3가지 데이터를 지닌다. 여기서 작업 경로와 커서 위치를 가공하여 얻었다.
//editor: vscode.TextEditorSelectionChangeEvent
const body: VSCodeStatusData = {
workspaceName:
`${vscode.workspace.name}/${vscode.workspace.asRelativePath(
editor.textEditor.document.fileName
)}`,
position: editor.selections.map((selection) => ({
start: {
char: selection.start.character,
line: selection.start.line,
},
end: {
char: selection.end.character,
line: selection.end.line,
},
})),
githubUrl: getGithubUrl(),
};
asRelativePath
는 주어진 절대 경로를 에디터의 루트 경로의 상대적인 경로로 변환해준다. /src/index.tsx
와 같이 되는데, 이 앞에 프로젝트 이름을 넣으면 Sharjects/src/index.tsx
작업 경로가 완성된다.
깃허브 레포지토리 링크를 가져오는건 생각보다 번거로웠는데, vscode.extension.getExtension(<string>);
함수는 타 확장을 그대로 가져올 수 있었으나 그것이 내장된 확장임에도 불구하고 타입이 명시되지 않았기 때문이다. 그래서 직접 명시 파일을 라이선스와 같이 프로젝트에 가져와서 쓰고 있다.
git.getAPI(1).repositories[0].state.remotes[0].fetchUrl
몇가지 시도로 메인 브렌치와 연결된 원격의 fetch url를 얻는 데 성공했다.
지금껏 네트워크 통신에선 fetch
를 사용했지만 이번엔 웹 표준 API가 없는 완전히 독립된 Node.js 환경에서 네트워크 통신을 해야 하므로 axios
를 대신 사용했다.
axios.post(
"https://sharjects-sharlottes.vercel.app/api/vscode/presence",
body
);
사전에 정의해둔 body를 백엔드 엔드포인트로 POST한 다음 포트폴리오 프로젝트로 돌아가 엔드포인트 헨들러를 작성하면 수신도 끝이다. 아주 잠깐 맛봤는데도 fetch
를 두고 axios
를 쓰는 사람들의 기분이 공감됐었다.
위 코드에선 절대 놓쳐선 안되는 치명적인 결함이 있었다. 바로 이벤트 리스너를 받을 때마다 무조건적인 수집 및 수신을 하는 것이다. 어차피 나 혼자 쓸 것이지만 커서가 바뀌는 빈도는 상상을 초월해서 예기치 못한 리소스 낭비가 될지도 모른다.
더 높은 안정성을 위해 일정 주기 내에 중복 호출되는 리스너는 무시될 필요가 있다.
이를 위해서 전에 만들어둔 throttle
함수를 사용했다.
type throttleType = <PT extends Array<any>, RT = void>(
callback: (...params: PT) => RT,
duration?: number
) => (...params: PT) => RT | undefined;
export const throttle: throttleType = (callback, duration = 100) => {
let id: NodeJS.Timeout | undefined;
return (...params) => {
if (id) return;
id = setTimeout(() => (id = undefined), duration);
return callback(...params);
};
};
함수가 실행되면 어떤 값으로 입구를 막고 duration
밀리초 후에 값을 비워두는 원리다.
vscode 확장을 불러오는 방법은 VSIX 압축파일로 로컬에서 가져오는 방법과 마켓플레이스로 클라우드에서 가져오는 방법이 있다. 이 확장은 오직 나 혼자만 쓸 계획이며 별도의 보안 처리를 해두지 않아 마켓플레이스에 퍼블리싱하는건 위험하다고 생각해서 페키징만 했다.
페키징 방법도 매우 쉽다. 확장을 툴킷으로 시작했으니 툴킷으로 끝낸다, vsce는 아주 쉽고 간단하게 확장을 페키징하여 VSIX 파일을 생성한다.
npm install -g @vscode/vsce
vsce package
로 빠르게 페키징할 수 있는데 package.json
에 repository
와 license
키가 없으면 경고하니 참고바란다. 아마 퍼블리싱 때 써먹는 것 같은게, 무시하고 계속 진행할 수도 있다.
vscode는 명령 팔레트에서 vsix 가져오기를 이미 지원해주고 있어서 명령어 실행 후 파일 선택만 하면 알아서 설치된다.
import type { NextApiRequest, NextApiResponse } from "next";
import type { VSCodeStatusData } from "src/@types";
let latestRecord: VSCodeStatusData | undefined;
export default (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") {
latestRecord = req.body;
} else {
res.status(200).json({ item: latestRecord });
}
};
POST로 들어오면 케시를 업데이트하고, GET로 들어오면 케시를 보내주는 간단한 api 헨들러 함수다.
더 높은 안정성을 위해 DB에 laetestRecord
를 저장할 필요가 있겠지만 배포 사이트는 무중단 배포로 이뤄지고 있고, 그리 중요한 데이터가 아니라 판단해 케싱으로 마무리했다.
const VscodeStatus: React.FC = () => {
const [data, setData] = React.useState<VSCodeStatusData>();
React.useEffect(() => {
fetch("/api/vscode/presence")
.then<{
item: VSCodeStatusData | undefined;
}>((data) => data.json())
.then((res) => setData(res.item));
}, []);
return <>{/* TODO: data로 이리저리 가공하기 */}</>;
}
앞서 만든 엔드포인트에 이번엔 POST가 아니라 GET로 요청하여 케싱된 데이터를 가져온다. 생각해보니 백엔드에서 케싱 데이터가 없으면 에러 코드를 던지고 프론트에서 catch
로 헨들링하는게 더 적절한지는 고민이 필요할 것 같다.
점점 뭔가가 늘어나니 뿌듯하다. 심지어 그 늘어난게 허울 좋은게 아닌 내실 탄탄하고 묵직한 시련의 산물이라 생각하니 세삼 뿌듯하다. 다음엔 wakatime api와 github api로 코딩 시간/일일 활동 통계를 내어볼 생각인데 이번엔 순탄하게 지나갔으면 좋겠다. 카드 하나 대비 들어간 노력이 근례에 가장 엄청나서 너무 지친다...