💡 이 글은 42서울 팔만코딩경에 게시된 내용과 동일합니다.
테스트코드
이너서클, 특히 저서클 과제를 수행하면서 여러분이 쓰셨던 francinette과 같은 테스터기를 만든다는건 꽤나 어려운 작업 같아보입니다. 맞습니다. 해당 테스터의 코드를 뜯어보면 복잡한 구조와 알 수 없는 코드들을 보며 눈이 어지럽습니다. 저도 당장 만들라면 못만들겠습니다.
하지만! js, java, python을 포함한 서비스에 사용되는 여러 고수준언어들은 테스트를 쉽게 만들 수 있는 라이브러리가 있어 누구든 쉽게 라이브러리를 활용해 테스트코드를 만들 수 있습니다. 이 글에서는 javascript, typescript에서의 테스트코드 작성에 대해 살펴보고자 합니다.
Vitest
Vite(개발자피셜 ‘비트’)의 개발자가 만든 테스트 프레임워크입니다. 문법은 기존 jest와 거의 비슷하며 여러 모킹 도구들을 내장하고 있고 성능도 좋아 개인적으로 애용하는 편입니다. NestJS의 경우는 jest를 거의 강제하기 때문에 사용하려면 NestJS의 여러 라이브러리와 레퍼런스를 포기해야합니다. Svelte-kit은 Vite를 사용해 번들링을 하기 때문에 오히려 호환이 좋습니다.(Svelte 만세)
Hello World!
간단한 테스트를 위해 새로운 패키지를 만들어주겠습니다.
mkdir 3mintest
cd 3mintest
npm init #yarn, pnpm도 상관없습니다.
vitest를 사용하기 위해선 의존성 설치를 먼저 해줘야 합니다.
npm i -D vitest
yarn add -D vitest
pnpm i -D vitest
공식문서의 요구사항에는 node 14.18 버전 이상, 3.0.0v 이상의 Vite를 요구하고 있지만 없어도 테스트는 잘 돌아가기 때문에 이번 글에서는 따로 다루지 않겠습니다.
저같은 경우는 npm init을 할 때 package.json에 test 명령어가 같이 만들어졌는데요, 이 명령어를 실행하면 vitest를 실행할 수 있도록 package.json을 수정해줍니다. 없으면 만들어주면 됩니다.
{
"name": "3mintest",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "vitest run" // 수정
},
"author": "",
"license": "ISC",
"devDependencies": {
"vitest": "^0.31.1"
}
}
이제 테스트를 실행시켜볼까요? 스크립트를 등록했기 때문에 쉽게 테스트를 작동시킬 수 있습니다.
npm test
테스트가 돌아가면 아래와 같은 결과화면이 터미널에 출력됩니다.
❯ npm test
> 3mintest@1.0.0 test
> vitest run
RUN v0.31.1 /Users/grasshopper42/study/3mintest
include: **/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}
exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*
watch exclude: **/node_modules/**, **/dist/**
No test files found, exiting with code 1
테스트 파일이 없어서 오류를 출력했습니다. 여기서 우리는 vitest가 어떻게 테스트파일을 감지하는지 알아볼 수 있습니다.
include: **/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}
**은 현재 폴더를 포함한 모든 하위폴더를 의미합니다. 즉, hello.test.js
, world.spec.js
와 같이 확장자 앞에 test나 spec을 붙여주면 됩니다. 아니면 파일명을 직접 입력해 테스트할수도 있습니다. 그럼 간단한 테스트파일을 하나 작성해보겠습니다.
// hello.test.js
import { describe, test, expect } from 'vitest';
describe("hello", () => {
// test 대신 it 함수를 사용해도 됨
test("world", () => {
expect(1).toBe(1);
});
});
describe
는 테스트의 집합입니다. 함수명처럼 이 테스트들이 어떤 의미를 지니는지 묶어서 표현할 수 있습니다. 한가지 기능에 여러 테스트가 필요한 경우 describe로 묶으면 결과창에도 폴더 트리처럼 묶어서 보여줍니다. describe 안에 또다른 descibe가 들어갈 수도 있습니다. test
는 테스트 함수를 의미합니다. it
을 써도 됩니다. 일반적으로 테스트케이스를 영어로 작성하면 it(”should be passed”, () ⇒ {})
이런식으로 많이 쓰기 때문에 it을 많이 쓰는 편입니다. expect
는 테스트 안에서 수행할 검사입니다. expect의 인자로 검증할 값을 넣고 메소드를 활용해 어떻게 검증할지를 적어주면 됩니다. 여기서는 toBe를 사용해 동일값인지 검사하고 있습니다. “1은 1이어야한다” 라는 의미입니다. 이제 테스트를 돌려봅시다.
❯ npm test
> 3mintest@1.0.0 test
> vitest run
RUN v0.31.1 /Users/grasshopper42/study/3mintest
✓ hello.test.js (1)
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 13:50:40
Duration 212ms (transform 16ms, setup 0ms, collect 6ms, tests 2ms, environment 0ms, prepare 44ms)
성공했다는 결과와 함께 여러 정보들이 나옵니다. 몇개의 테스트파일, 몇개의 테스트, 언제시작해서 얼마나 걸렸는지 등이요. 이제 실패한 경우를 보겠습니다. 파일에 새로운 테스트를 추가해봅시다.
// hello.test.js
//...기존 테스트
test("fail", () => {
expect(1).toBe(2);
});
});
1은 2여야한다! 라고 테스트를 만들었습니다. 그리고 테스트를 실행시키면 이런 실패 화면을 볼 수 있습니다.
❯ npm test
> 3mintest@1.0.0 test
> vitest run
RUN v0.31.1 /Users/grasshopper42/study/3mintest
❯ hello.test.js (2)
❯ hello (2)
✓ world
× fail
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL hello.test.js > hello > fail
AssertionError: expected 1 to be 2 // Object.is equality
❯ hello.test.js:9:15
7|
8| test("fail", () => {
9| expect(1).toBe(2);
| ^
10| });
11| });
- Expected - 0
+ Received + 1
- 2
+ 1
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
Start at 13:57:04
Duration 183ms (transform 16ms, setup 0ms, collect 6ms, tests 6ms, environment 0ms, prepare 43ms)
아까는 파일명만 보여줬던 것과는 다르게 이젠 어떤 테스트가 통과했고, 어떤 테스트가 실패했는지도 보입니다. 그리고 어디서 실패했는지도 자세하게 보여줍니다. 기대값은 2인데 인자로 들어온 값은 1이라는 이야기입니다. 대충 어떤 방식으로 사용하면 될지 알 수 있겠죠?
활용
이제 좀 더 디테일한 예시를 들어봅시다. 카뎃이라는 객체가 있습니다. 카뎃은 월렛을 가지고 있고, 월렛을 사용해 상품을 살 수 있는 기능을 구현하려 합니다. 우리는 어떤걸 테스트해볼 수 있을까요?
- 상품을 사면, 상품 비용만큼의 월렛이 차감되어야한다.
- 월렛이 부족하면 상품을 살 수 없다.
라는 중요 비즈니스로직을 검증해볼 수 있겠습니다. 필요한 테스트를 먼저 작성해보겠습니다.
// BuyProduct.test.ts
describe("상품 구매", () => {
test.todo("상품을 구매하면 월렛이 차감된다.", () => {
// given
// 1000원을 보유한 카뎃
// 500원짜리 상품
// when
// 상품을 구매한다
// then
// 월렛에 500원이 남아있다
});
test.todo("월렛이 부족하면 상품을 구매할 수 없다.", () => {
// given
// 1000원을 보유한 카뎃
// 1500원짜리 상품
// when
// 상품을 구매한다
// then
// 상품을 구매할 수 없다는 오류를 출력한다.
// 월렛에 1000원이 남아있다.
});
});
todo메소드를 이용하면 코드를 작성하더라도 테스트에서는 무시됩니다. 좀 더 실용적인 테스트코드 작성을 위해 많이 사용되는 given-when-then기법을 사용해 보았습니다. 주어진 조건(given), 행위(when), 결과(then)로 나눠 코드의 가독성을 높이고 보다 의미있는 테스트를 작성하기 위한 기법입니다. 이제 주석을 실제 코드로 바꿔야겠죠?
// BuyProduct.test.ts
describe("상품 구매", () => {
test("상품을 구매하면 월렛이 차감된다.", () => {
// given
const cadet = new Cadet(1000);
const product = new Product(500);
// when
cadet.buy(product);
// then
expect(cadet.wallet).toBe(500);
});
test("월렛이 부족하면 상품을 구매할 수 없다.", () => {
// given
const cadet = new Cadet(1000);
const product = new Product(500);
// when
const action = () => cadet.buy(product);
// then
expect(action).toThrow("월렛이 부족합니다.");
expect(cadet.wallet).toBe(1000);
});
});
테스트를 확인해야하니 todo는 지워줍시다. 그리고 테스트를 돌리면 당연히 실패합니다.
❯ npm test
> 3mintest@1.0.0 test
> vitest run
RUN v0.31.1 /Users/grasshopper42/study/3mintest
❯ BuyProduct.test.js (2)
❯ 상품 구매 (2)
× 상품을 구매하면 월렛이 차감된다.
× 월렛이 부족하면 상품을 구매할 수 없다.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL BuyProduct.test.js > 상품 구매 > 상품을 구매하면 월렛이 차감된다.
ReferenceError: Cadet is not defined
❯ BuyProduct.test.js:26:19
24| test("상품을 구매하면 월렛이 차감된다.", () => {
25| // given
26| const cadet = new Cadet(1000);
| ^
27| const product = new Product(500);
28|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
FAIL BuyProduct.test.js > 상품 구매 > 월렛이 부족하면 상품을 구매할 수 없다.
ReferenceError: Cadet is not defined
❯ BuyProduct.test.js:38:19
36| test("월렛이 부족하면 상품을 구매할 수 없다.", () => {
37| // given
38| const cadet = new Cadet(1000);
| ^
39| const product = new Product(1500);
40|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
Test Files 1 failed (1)
Tests 2 failed (2)
Start at 14:32:38
Duration 176ms (transform 16ms, setup 0ms, collect 7ms, tests 4ms, environment 0ms, prepare 35ms
아직 Cadet와 Product 객체를 만들지 않았거든요! js인 만큼 literal object나 prototype을 활용해 객체를 만들 수도 있겠지만 편의를 위해 클래스 방식을 사용하겠습니다. Cadet은 wallet이라는 속성을 갖고있어야하고, buy라는 메소드가 필요합니다. product는 가격에 대한 속성이 필요하겠죠?
class Cadet {
wallet = 0;
constructor(wallet) {
this.wallet = wallet
}
buy(product) {}
}
class Product {
price = 0;
constructor(price) {
this.price = price;
}
}
이제 다시 테스트를 시도해봅시다.
❯ BuyProduct.test.js (2)
❯ 상품 구매 (2)
× 상품을 구매하면 월렛이 차감된다.
× 월렛이 부족하면 상품을 구매할 수 없다.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL BuyProduct.test.js > 상품 구매 > 상품을 구매하면 월렛이 차감된다.
AssertionError: expected 1000 to be 500 // Object.is equality
❯ BuyProduct.test.js:33:26
31|
32| // then
33| expect(cadet.wallet).toBe(500);
| ^
34| });
35|
- Expected - 0
+ Received + 1
- 500
+ 1000
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
FAIL BuyProduct.test.js > 상품 구매 > 월렛이 부족하면 상품을 구매할 수 없다.
AssertionError: expected [Function action] to throw an error
❯ BuyProduct.test.js:45:20
43|
44| // then
45| expect(action).toThrow("월렛이 부족합니다.");
| ^
46| expect(cadet.wallet).toBe(1000);
47| });
- Expected - 0
+ Received + 1
- null
+ undefined
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
Test Files 1 failed (1)
Tests 2 failed (2)
Start at 14:38:03
Duration 190ms (transform 17ms, setup 0ms, collect 8ms, tests 8ms, environment 0ms, prepare 42ms)
구매기능을 구현하지 않아서 상품을 구매해도 월렛이 차감되지 않습니다. 그럼 상품을 구매하면 상품의 가격만큼 월렛이 차감되도록 구현해봅시다.
class Cadet {
wallet = 0;
constructor(wallet) {
this.wallet = wallet
}
buy(product) {
wallet -= product.price;
}
}
이제 상품 구매가 제대로 이루어지는지 확인해볼까요?
❯ BuyProduct.test.js (2)
❯ 상품 구매 (2)
✓ 상품을 구매하면 월렛이 차감된다.
× 월렛이 부족하면 상품을 구매할 수 없다.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
FAIL BuyProduct.test.js > 상품 구매 > 월렛이 부족하면 상품을 구매할 수 없다.
AssertionError: expected [Function action] to throw an error
❯ BuyProduct.test.js:45:20
43|
44| // then
45| expect(action).toThrow("월렛이 부족합니다.");
| ^
46| expect(cadet.wallet).toBe(1000);
47| });
- Expected - 0
+ Received + 1
- null
+ undefined
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
Start at 14:41:39
Duration 181ms (transform 18ms, setup 0ms, collect 8ms, tests 6ms, environment 0ms, prepare 41ms)
네! 이제 비용이 부족할 때 에러를 반환하도록 수정해면 되겠네요.
class Cadet {
wallet = 0;
constructor(wallet) {
this.wallet = wallet
}
buy(product) {
if (product.price > this.wallet) {
throw new Error("월렛이 부족합니다.");
}
wallet -= product.price;
}
}
그리고 테스트를 수행하면?
❯ npm test
> 3mintest@1.0.0 test
> vitest run
RUN v0.31.1 /Users/grasshopper42/study/3mintest
✓ BuyProduct.test.js (2)
Test Files 1 passed (1)
Tests 2 passed (2)
Start at 14:44:05
Duration 182ms (transform 18ms, setup 0ms, collect 8ms, tests 2ms, environment 0ms, prepare 44ms)
모든 테스트가 성공적으로 수행됐습니다. 테스트를 위한 더 많은 기능들은 공식문서를 참고하시는걸 추천드립니다.
TDD
3분 완성 테스트코드 치고는 많은 내용을 다뤘습니다. 방금 상품구매기능을 구현하면서 사용한 방법이 바로 TDD(Test Driven Development), 테스트 주도 개발입니다. 테스트를 활용하면 이렇게 요구사항을 먼저 분석한 후 실패하는 테스트케이스를 만들고 테스트를 하나씩 성공시켜가며 개발해나가는 방법을 활용할 수 있습니다. TDD에서는 테스트케이스가 곧 기능의 지표이며 테스트를 모두 통과하면 모든 기능을 완벽하게 수행한다고 판단하고 더이상의 코드를 추가하지 않습니다. 불필요한 부분들을 정리하며 리팩토링은 하지만요.
테스트는 리팩토링 과정에서도 힘을 발휘합니다. 코드를 수정하는 과정에서 테스트를 계속 돌려보면서 안정성을 지키며 리팩토링을 할 수 있게 됩니다. 하지만, 테스트를 하다보면 구조적 고민들을 하게 될 수 있습니다. 방대한 기능을 담은 테스트를 작성하기란… 이제 어떻게 하면 쉽고 의미있는 테스트를 작성할 수 있을지를 고민하고, 코드를 분리합니다. OOP(객체지향), FP(함수형), 그리고 여러 아키텍쳐들이 멀고 어렵게만 느껴지셨다면 테스트를 시도해보세요. 작은 기능이라도 그 개념과 원칙들이 큰 도움이 되는걸 발견할 수 있습니다.
긴 글 읽어주셔서 감사합니다🙇🏽♂️