Mincho 프로젝트의 이번주 #2 - RFC-TDD 프로세스로 개발하기
안녕하세요?
벌써 두번째 뉴스레터를 적어야 할 때가 돌아왔습니다!!
뉴스레터에서는 개발 현황 뿐만 아니라, 각종 개발 팁들을 조금씩 풀어보려고 해요.
이번에 소개해드릴 것은 RFC-TDD 프로세스로 개발하기입니다.
소개글
주간레터
Mincho 프로젝트의 이번주 #1 - 팀 빌딩하는 방법
Mincho 프로젝트의 이번주 #2 - RFC-TDD 프로세스로 개발하기 (현재)
Mincho 프로젝트의 이번주 #3 - 깃허브 봇들과 함께해요
Mincho 프로젝트의 이번주 #4 - JSON 콘솔 디버깅 라이브러리 배포
개요
Mincho 프로젝트의 이번주 #1 - 팀 빌딩하는 방법에서 설명한 것처럼 RFC 문서를 작성하면 장점이 많습니다.
스펙 구성, 컨텍스트 공유, 테스트 짜기 등등에서요.
제가 하는 방법을 설명하자면, 다음과 같습니다.
RFC 문서 작성
RFC를 보면서 in source 테스트 케이스 추가
it.only()로 해당 테스트만 실행되게 해놓고, debugger 모드로 실행
의심가는 부분을 break 걸어놓고, 필요하면 log 추가
코드가 헷갈리면 IDE 옆에 켜진 AI에게 물어보기
혹시 구현부분에 추가적인 설계가 필요할 경우 RFC에 적어서 PR 및 커밋
다시 RFC 보면서 코드 구현
only 옵션 풀어보고, 돌아가면 전체 테스트 한번 돌리기
PR 및 Merge
반복
뭔가 복잡해보이죠?
그렇다면 우선 많이들 하는 TDD로 개발하기만 생각해봅시다.
TDD로 개발하기
이번에 한 작업은 중첩된 CSS 변환이었는데요,
다음과 같이 범위를 줄여봤어요. 훨씬 쉽죠?
in source 테스트 케이스 추가
it.only()로 해당 테스트만 실행되게 해놓고, debugger 모드로 실행
의심가는 부분을 break 걸어놓고, 필요하면 log 추가
구현
only 옵션 풀어보고, 돌아가면 전체 테스트 한번 돌리기
1. in source 테스트 케이스 추가
저희 프로젝트는 탄탄하게 만들기 위해서 in source 테스트를 많이 만듭니다.
in source 테스트가 뭐냐고요?
말 그대로 소스코드와 테스트코드를 함께두는 방식을 말합니다.
Rust에서 자주 쓰는 방법이고, 저희는 Vitest를 통해 달성하고 있어요.
// == Interface ================================================================
export function isCSSVarKey(keyStr: string) {
return keyStr.startsWith("$");
}
export function isPureCSSVarKey(keyStr: string) {
return keyStr.startsWith("--");
}
export function isVarsKey(keyStr: string) {
return keyStr === "vars";
}
export function replaceCSSVarKey(keyStr: string) {
return convertToCSSVar(keyStr);
}
// == Tests ====================================================================
if (import.meta.vitest) {
const { describe, it, expect } = import.meta.vitest;
describe.concurrent("Replace CSS Var value", () => {
it("Is css var key", () => {
expect(isCSSVarKey("$myCssVariable")).toBeTruthy();
expect(isPureCSSVarKey("--my-css-variable")).toBeTruthy();
expect(isCSSVarKey("-my-css-variable")).toBeFalsy();
expect(isPureCSSVarKey("-my-css-variable")).toBeFalsy();
expect(isCSSVarKey("_hover")).toBeFalsy();
expect(isPureCSSVarKey("_hover")).toBeFalsy();
});
it("Convert to css var", () => {
expect(replaceCSSVarKey("$myCssVariable")).toBe("--my-css-variable");
expect(replaceCSSVarKey("$my-css-variable")).toBe("--my-css-variable");
});
});
}
이렇게 하면 몇가지 장점이 있어요.
한가지 파일에서 관련된 소스코드와 테스트 코드가 함께 있으므로 작성, 분석, 추적이 쉽습니다.
작성: 화면 스플릿만 하여 바로 작성할 수 있어요.
분석: 한 파일에 있으므로, 타입만 보고 인터페이스를 추정하기 힘든 경우 아래로 내려서 예제를 바로 볼 수 있습니다.
추적: git에서 변경 사항이 여러 파일과 디렉토리에 걸쳐있지 않아 변경사항 추적이 간편합니다. 혹시 test파일을 추가하지 않았다던가 하는 일도 없겠죠.
유닛 테스트와 통합 테스트를 분리할 수 있습니다. (in-source에 있는게 유닛테스트)
테스트를 작성하는 방법 자체는 다음을 참고해보세요.
새로운 기능이나 버그를 고치기 위한 목적으로, 테스트를 작성했다면 분명 실패할거에요.
아직 기능이 구현되거나 버그가 고쳐지지 않았으니까요.
2. 한가지 테스트만 실행되게 해놓고, debugger 모드로 실행
두번째는 작성한 한가지 테스트만 실행되게 해야합니다.
다른 테스트까지 실행하면 분석하기가 어려워지니까요.
이를 위해 먼저 only 옵션을 주도록 합니다.
it.only("Nested selector & AtRules", () => {
// 작성한 테스트...
});
다음은 Debugger 모드로 현재 파일만 watch로 실행되도록 .vscode/launch.json 파일을 구성합니다.
이렇게 하면 디버그 모드로 현재파일 테스트를 바로 실행하고, 저장될 때마다 테스트가 통과하는지 확인할 수 있어요.
{
// From https://vitest.dev/guide/debugging#vs-code
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"program": "${input:GIT_ROOT}/node_modules/vitest/vitest.mjs",
"args": [
"watch", "${relativeFile}"
],
"smartStep": true,
"outFiles": ["${workspaceRoot}/dist/**/*.js"],
"console": "integratedTerminal",
"cwd": "${workspaceRoot}",
}
],
// Need to install `augustocdias.tasks-shell-input` extension
"inputs": [
{
"id": "GIT_ROOT",
"type": "command",
"command": "shellCommand.execute",
"args": {
"command": "git rev-parse --show-toplevel",
"useSingleResult": true,
"useFirstResult": true
}
}
]
}
한번 실행해 볼까요?
파일을 저장하니 다시 실행되는 모습을 볼 수 있습니다.

3. 의심가는 부분을 break 걸어놓고, 필요하면 log 추가
이제 브레이크 포인트를 두고, 값들을 확인하면서 코딩이 가능해요.

물론 필요하다면 console.log도 찍어도 좋습니다.
보다 이쁘게 보려면 JSON.stringify(object, null, 2) 를 함께 쓰면 되요.
4. 구현 및 커밋
이제 테스트가 통과할 때까지 구현만 하고, only 옵션을 푼다음 테스트하고 커밋하면 끝나겠죠?
이렇게 디버거와 함께 반복적으로 테스트가 통과하는지 체크하면 개발은 편하면서 일정 품질은 보장이 되요.
RFC와 함께하기
그렇다면 RFC는 Test와 함께 어떻게 시너지가 있을까요?
RFC란 "비평을 기다리는 문서"라는 의미로, 새로운 아이디어에 대한 전문가 비평을 받거나 정보를 공유가 목적입니다.
한 번 발행된 RFC는 폐기되지 않으며, 수정이 필요한 경우 새로운 RFC로 발행됩니다.
저희는 각종 기능의 스펙과 배경, 구현 방법등을 담아 발행하고 있습니다.
그리고 테스트를 작성할 때는 RFC에 기반하여 작성하는데요,
설계가 잘못되었거나 보충되어야 할 때를 빠르게 판단할 수 있습니다.
예를들어 CSS 중첩기능을 구현했어야 했는데 다음은 어떻게 변환이 되어야 할까요?
그리고 각종 at-rule들과 복잡한 selector가 중첩이 되면 어떻게 할까요?
const myCss = css({
"nav li > &": {
"@media (prefers-color-scheme: dark)": {
_hover: {
background: "red"
}
}
}
});
그래서 저는 중첩된 순서가 미리 정해져 있도록 RFC문서를 보충한 후에 기능을 구현했어요.
이제 위 코드는 명시적으로 다음과 같이 변환이 되어야 하고요,
export const myCss = style({
"@media": {
"(prefers-color-scheme: dark)": {
selectors: {
"nav li > &:hover": {
background: "red"
}
}
}
}
});
훨씬 복잡한 상황도 중첩된 순서가 항상 같으므로 안정적인 출력이 가능해져요.
export const myCss = style({
// Level1: @layer
"@layer": {
"framework.layout": {
// Level2: @supports
"@supports": {
"gap: 1rem": {
// Level3: @media
"@media": {
"(prefers-color-scheme: dark) and (prefers-reduced-motion)": {
// Level4: @container
"@container": {
"(min-width: 500px)": {
// Level5: selectors
"selectors": {
"nav li > &:hover": {
background: "red"
}
}
}
}
}
}
}
}
}
}
});
모아놓음 RFC문서만 보면 각종 기능과 설계를 한눈에 볼 수 있으므로 개발하기가 매우 편해진답니다.
정리하자면
형태이고요,
이를 공정으로 생각하면
로 깔끔하게 분할이 됩니다.
이게 정답이다!!라고 할 수는 없겠지만 몇달 쉬었다가 개발을 시작함에도 적응이 무척 쉽네요.
여러분도 RFC - TDD 프로세스로 작업해보시는 것은 어떨까요?
둘째주 한일
CSS Nesting RFC에서 중첩된 at-rules와 selector 기능이 모두 구현됐어요.
또한 방치되어 있던 의존성을 모두 업데이트 했습니다.
ESLint 설정도 flat config로 바꾸었어요.
CSS Nesting RFC가 끝나면 첫번째 목표인 "Natural CSS in the Typescript "가 이루어지는 셈이니 릴리즈까지는 단 2개의 기능만 남은 셈입니다.
그럼 다음주 출시를 목표로..!! 열심히 해보겠습니다.
담주에 만나요~~
혹시 개발에 관심이 있으시다면 댓글을 달아주시거나 alstjr7375@daum.net에 연락을 주셔도 좋습니다.
하단의 업보트와 깃허브 스타를 찍어주셔도 매우 큰 도움이 됩니다!!