Skip to content
Branch: master
Find file Copy path
Find file Copy path
6 contributors

Users who have contributed to this file

@ragubyun @sury05 @min0418 @devori @icepeng @dubzzz
1958 lines (1197 sloc) 113 KB

πŸ‘‡ 이 κ°€μ΄λ“œκ°€ λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈ κΈ°μˆ μ„ ν•œ 단계 λŒμ–΄ μ˜¬λ¦¬λŠ” 이유

πŸ“— μ² μ €ν•˜κ³  맀우 포괄적인 45가지 μ΄μƒμ˜ λͺ¨λ²” 사둀

JavaScript 및 Node.js에 λŒ€ν•œ AλΆ€ν„° ZκΉŒμ§€μ˜ λ―ΏμŒμ§ν•œ κ°€μ΄λ“œμž…λ‹ˆλ‹€. μˆ˜μ‹­ 가지 졜고의 λΈ”λ‘œκ·Έ κ²Œμ‹œλ¬Ό, μ„œμ  및 도ꡬλ₯Ό μš”μ•½ν•˜κ³  μ •λ¦¬ν•©λ‹ˆλ‹€.

🚒 기초λ₯Ό λ›°μ–΄λ„˜μ–΄ κ³ κΈ‰μœΌλ‘œ

μš΄μ˜μ€‘μΈ μ œν’ˆμ˜ ν…ŒμŠ€νŠΈ, λŒμ—°λ³€μ΄ ν…ŒμŠ€νŠΈ, 속성 기반 ν…ŒμŠ€νŠΈ 및 기타 μ—¬λŸ¬ μ „λž΅μ  & μ „λ¬Έ 도ꡬ와 같은 κ³ κΈ‰ 주제둜 λ„˜μ–΄κ°€λŠ” 여정을 κ²½ν—˜ν•˜μ‹­μ‹œμ˜€. 이 κ°€μ΄λ“œμ˜ λͺ¨λ“  단어λ₯Ό 읽으면 λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈ 기술이 평균보닀 λ†’μ•„μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€.

🌐 Full-stack: ν”„λ‘ νŠΈ, λ°±μ—”λ“œ, CI, 무엇이든

λͺ¨λ“  μ‘μš©ν”„λ‘œκ·Έλž¨ κ³„μΈ΅μ˜ κΈ°μ΄ˆκ°€ λ˜λŠ” μœ λΉ„μΏΌν„°μŠ€ ν…ŒμŠ€νŠΈ 방법을 μ΄ν•΄ν•˜λŠ” κ²ƒμœΌλ‘œλΆ€ν„° μ‹œμž‘ν•˜μ‹­μ‹œμ˜€. 그런 λ‹€μŒ ν”„λ‘ νŠΈμ—”λ“œ/UI, λ°±μ—”λ“œ, CI ν˜Ήμ€ 이 λͺ¨λ“ κ²ƒμ„ κ³΅λΆ€ν•˜μ„Έμš”.

Yoni Goldberg μž‘μ„±


μ„Ήμ…˜ 0: ν™©κΈˆλ₯ 

λͺ¨λ“  λͺ¨λ“  μ‚¬λžŒλ“€μ—κ²Œ μ˜κ°μ„ μ£ΌλŠ” ν•˜λ‚˜μ˜ μ‘°μ–Έ(ν•˜λ‚˜μ˜ νŠΉμˆ˜ν•œ ν•­λͺ©)

μ„Ήμ…˜ 1: ν…ŒμŠ€νŠΈ ν•΄λΆ€

기초 - κΉ”λ”ν•œ ν…ŒμŠ€νŠΈ κ΅¬μ„±ν•˜κΈ°(12개)

μ„Ήμ…˜ 2: λ°±μ—”λ“œ

λ°±μ—”λ“œ 및 λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ ν…ŒμŠ€νŠΈ 효율적으둜 μž‘μ„±ν•˜κΈ°(8개)

μ„Ήμ…˜ 3: ν”„λ‘ νŠΈμ—”λ“œ

μ»΄ν¬λ„ŒνŠΈ 및 E2E ν…ŒμŠ€νŠΈλ₯Ό ν¬ν•¨ν•œ μ›Ή UI에 λŒ€ν•œ ν…ŒμŠ€νŠΈ μž‘μ„±ν•˜κΈ°(11개)

μ„Ήμ…˜ 4: ν…ŒμŠ€νŠΈ 효과 μΈ‘μ •

κ°μ‹œμžλ₯Ό κ°μ‹œν•˜κΈ° - ν…ŒμŠ€νŠΈ ν’ˆμ§ˆ μΈ‘μ •(4개)

μ„Ήμ…˜ 5: 지속적인 톡합

μžλ°”μŠ€ν¬λ¦½νŠΈ μ„Έκ³„μ—μ„œ CI에 λŒ€ν•œ 지침(9개)

μ„Ήμ…˜ 0️⃣: ν™©κΈˆλ₯ 

βšͺ ️ 0. ν™©κΈˆλ₯ : λ¦° ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ 섀계

βœ… μ΄λ ‡κ²Œ 해라: ν…ŒμŠ€νŠΈ μ½”λ“œλŠ” μ œν’ˆ μ½”λ“œμ™€ λ‹€λ¦…λ‹ˆλ‹€. λ‹¨μˆœν•˜κ³ , 짧고, 좔상화가 μ—†κ³ , λ¬΄λ‚œν•˜κ³ , μž‘μ—…ν•˜κΈ°μ— νŽΈλ¦¬ν•˜κ³ , λ¦°ν•˜κ²Œ λ””μžμΈ ν•˜μ‹­μ‹œμ˜€. ν…ŒμŠ€νŠΈλ₯Ό 보고 μ¦‰μ‹œ 의미λ₯Ό μ•Œμ•„μ±Œ 수 μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.

우리 머리속은 μ œν’ˆ μ½”λ“œλ‘œ κ°€λ“ν•˜κ³  뢀가적인 λ³΅μž‘ν•œ 것듀을 생각할 μ—¬μœ κ°€ μ—†μŠ΅λ‹ˆλ‹€. 또 λ‹€λ₯Έ μ–΄λ €μš΄ μ½”λ“œλ₯Ό μ–΅μ§€λ‘œ 생각해내렀고 ν•œλ‹€λ©΄, νŒ€μ˜ 속도λ₯Ό λŠ¦μΆ”κ²Œ λ˜μ–΄ μš°λ¦¬κ°€ ν…ŒμŠ€νŠΈλ₯Ό ν•˜λŠ” μ΄μœ κ°€ 무색해 질 κ²ƒμž…λ‹ˆλ‹€. μ‹€μ œλ‘œ λ§Žμ€ νŒ€λ“€μ΄ 이런 이유λ₯Ό ν…ŒμŠ€νŠΈλ₯Ό ν¬κΈ°ν•©λ‹ˆλ‹€.

ν…ŒμŠ€νŠΈλŠ” μΉœμ ˆν•˜κ³  μ›ƒλŠ” λ™λ£Œμ™€ ν•¨κ»˜ μΌν•˜λŠ” 것이 즐거울 수 μžˆλŠ” 기회이고, 적은 투자둜 큰 κ°€μΉ˜λ₯Ό μ œκ³΅ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 과학은 μš°λ¦¬μ—κ²Œ 두 개의 λ‡Œ μ‹œμŠ€ν…œμ΄ μžˆλ‹€κ³  λ§ν•©λ‹ˆλ‹€. 빈 λ„λ‘œμ—μ„œ μžλ™μ°¨λ₯Ό μš΄μ „ν•˜λŠ” λ“±μ˜ κ°„νŽΈν•œ ν™œλ™μ— μ‚¬μš©λ˜λŠ” μ‹œμŠ€ν…œ 1, 그리고 μˆ˜ν•™ 방정식을 ν‘ΈλŠ” 것과 같이 λ³΅μž‘ν•˜κ³  μ˜μ‹μ μΈ 연산을 μœ„ν•œ μ‹œμŠ€ν…œ 2. ν…ŒμŠ€νŠΈ μ½”λ“œλ₯Ό λ³Ό λ•Œ μˆ˜ν•™ 문제λ₯Ό ν‘ΈλŠ” 것 κ°™μ€κ²Œ μ•„λ‹Œ, HTML λ¬Έμ„œλ₯Ό μˆ˜μ •ν•˜λŠ” κ²ƒλ§Œ 큼 μ‰¬μ›Œμ•Όν•˜λŠ” μ‹œμŠ€ν…œ 1에 맞게 ν…ŒμŠ€νŠΈλ₯Ό μ„€κ³„ν•˜μ‹­μ‹œμ˜€.

선택적인 체리픽 기술, 툴 그리고 λΉ„μš©-효율적이고 λ›°μ–΄λ‚œ ROIλ₯Ό μ œκ³΅ν•˜λŠ” ν…ŒμŠ€νŠΈ λŒ€μƒ μ„ μ •μœΌλ‘œ μ΄λŸ¬ν•œ λͺ©μ μ„ 달성할 수 μžˆμŠ΅λ‹ˆλ‹€. ν•„μš”ν•œ 만큼의 ν…ŒμŠ€νŠΈ, μœ΅ν†΅μ„± 있게 μœ μ§€ν•˜λ €λŠ” λ…Έλ ₯, λ•Œλ‘œλŠ” μ• μžμΌν•¨κ³Ό λ‹¨μˆœμ„±μ„ μœ„ν•΄ 일뢀 ν…ŒμŠ€νŠΈμ™€ 신뒰성을 ν¬κΈ°ν•˜λŠ” 것도 κ°€μΉ˜κ°€ μžˆμŠ΅λ‹ˆλ‹€.

alt text

μ•„λž˜ λŒ€λΆ€λΆ„μ˜ 쑰언은 이 μ›μΉ™μ˜ νŒŒμƒμž…λ‹ˆλ‹€.

μ‹œμž‘ν•  μ€€λΉ„ λ˜μ…¨λ‚˜μš”?

μ„Ήμ…˜ 1: ν…ŒμŠ€νŠΈ ν•΄λΆ€

βšͺ ️ 1.1 각 ν…ŒμŠ€νŠΈ 이름은 μ„Έ λΆ€λΆ„μœΌλ‘œ κ΅¬μ„±λœλ‹€.

βœ… μ΄λ ‡κ²Œ 해라: ν…ŒμŠ€νŠΈλŠ” ν˜„μž¬ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ κ°œμ •νŒμ΄ μš”κ΅¬ 사항을 μΆ©μ‘±ν•˜λŠ”μ§€ μ—¬λΆ€λ₯Ό λ‹€μŒκ³Ό 같은 μ‚¬λžŒλ“€μ—κ²Œ μ•Œλ €μ•Όν•©λ‹ˆλ‹€: 배포λ₯Ό ν•  ν…ŒμŠ€ν„°, DevOps μ—”μ§€λ‹ˆμ–΄, 2λ…„ ν›„μ˜ λ―Έλž˜μ— μ½”λ“œκ°€ μ΅μˆ™ν•˜μ§€ μ•Šμ€ μ‚¬λžŒ. ν…ŒμŠ€νŠΈκ°€ μš”κ΅¬ 사항 μˆ˜μ€€μ—μ„œ μž‘μ„±λ˜μ–΄ 있고 μ„Έ λΆ€λΆ„μœΌλ‘œ κ΅¬μ„±λ˜μ–΄ μžˆλ‹€λ©΄, λͺ©μ μ„ 이룰 수 μžˆμŠ΅λ‹ˆλ‹€:

(1) 무엇을 ν…ŒμŠ€νŠΈν•˜κ³  μžˆλŠ”κ°€? 예) μ œν’ˆμ„œλΉ„μŠ€.μƒˆμ œν’ˆμΆ”κ°€ λ©”μ„œλ“œ

(2) μ–΄λ–€ 상황과 μ‹œλ‚˜λ¦¬μ˜€μ—μ„œ? 예) λ©”μ„œλ“œμ— 가격이 μ „λ‹¬λ˜μ§€ μ•ŠλŠ”λ‹€.

(3) μ˜ˆμƒλ˜λŠ” κ²°κ³ΌλŠ” 무엇인가? 예) μ‹ μ œν’ˆμ€ μŠΉμΈλ˜μ§€ μ•ŠλŠ”λ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: 배포에 μ‹€νŒ¨ν•˜μ˜€κ³  "μ œν’ˆ μΆ”κ°€" λΌλŠ” ν…ŒμŠ€νŠΈμ— μ‹€νŒ¨ν•˜μ˜€λ‹€. 이것이 μ •ν™•νžˆ μ–΄λ–€ μ˜€μž‘λ™ 인지λ₯Ό μ•Œλ €μ£Όλ‚˜μš”?

πŸ‘‡ 주의: 각 κΈ€μ—λŠ” 예제 μ½”λ“œκ°€ 있으며 λ•Œλ‘œλŠ” 이미지도 μžˆμŠ΅λ‹ˆλ‹€. ν΄λ¦­ν•˜μ—¬ ν™•μž₯

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: μ„Έ λΆ€λΆ„μœΌλ‘œ κ΅¬μ„±λœ ν…ŒμŠ€νŠΈ 이름

//1. λ‹¨μœ„ ν…ŒμŠ€νŠΈ
describe('μ œν’ˆ μ„œλΉ„μŠ€', function() {
  describe('μƒˆ μ œν’ˆ μΆ”κ°€', function() {
    //2. μ‹œλ‚˜λ¦¬μ˜€ 3. μ˜ˆμƒ
    it('가격을 μ§€μ •ν•˜μ§€ μ•ŠμœΌλ©΄ μ œν’ˆ μƒνƒœλŠ” 승인 λŒ€κΈ°μ€‘μ΄λ‹€.', ()=> {
      const newProduct = new ProductService().add(...);
      expect(newProduct.status).to.equal('승인 λŒ€κΈ°');

πŸ‘ μ˜¬λ°”λ₯Έ 예: μ„Έ λΆ€λΆ„μœΌλ‘œ κ΅¬μ„±λœ ν…ŒμŠ€νŠΈ 이름

alt text

βšͺ ️ 1.2 AAA νŒ¨ν„΄μ— μ˜ν•œ ν…ŒμŠ€νŠΈ ꡬ쑰

βœ… μ΄λ ‡κ²Œ 해라: 3개의 잘 잘 κ΅¬λΆ„λœ μ„Ήμ…˜ AAA(Arrange, Act, Assert)으둜 ν…ŒμŠ€νŠΈλ₯Ό κ΅¬μ„±ν•˜μ‹­μ‹œμ˜€. 이 ꡬ쑰λ₯Ό λ”°λ₯΄λ©΄ ν…ŒμŠ€νŠΈλ₯Ό μ‰½κ²Œ 읽을 수 μžˆμŠ΅λ‹ˆλ‹€:

첫번째 A - Arrange(μ€€λΉ„): ν…ŒμŠ€νŠΈκ°€ λͺ©ν‘œλ‘œ ν•˜λŠ” μ‹œλ‚˜λ¦¬μ˜€μ— ν•„μš”ν•œ μ‹œμŠ€ν…œμ„ μ œκ³΅ν•˜κΈ° μœ„ν•œ λͺ¨λ“  μ„€μ • μ½”λ“œ. μ—¬κΈ°μ—λŠ” ν…ŒμŠ€νŠΈ μƒμ„±μžμ˜ λ‹¨μœ„ μΈμŠ€ν„΄μŠ€ν™”, DB 데이터 μΆ”κ°€, 객체에 λŒ€ν•œ mock/stub 및 기타 μ€€λΉ„ μ½”λ“œκ°€ 포함될 수 μžˆμŠ΅λ‹ˆλ‹€.

λ‘λ²ˆμ§Έ A - Act(행동): λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰. 일반적으둜 μ½”λ“œ ν•œμ€„

μ„Έλ²ˆμ§Έ A - Assert(μ£Όμž₯, μ˜ˆμƒ): 받은 μ˜ˆμƒκ°’μ΄ μΆ©μ‘±ν•˜λŠ”μ§€ ν™•μΈν•˜μ‹­μ‹œμ˜€. 일반적으둜 μ½”λ“œ ν•œμ€„

❌ 그렇지 μ•ŠμœΌλ©΄: ν…ŒμŠ€νŠΈλŠ” 였늘 일의 μ•„μ£Ό λ‹¨μˆœν•œ 뢀뢄에 λΆˆκ³Όν•˜μ§€λ§Œ, 메인 μ½”λ“œλ₯Ό μ΄ν•΄ν•˜λŠ”λ° λ§Žμ€ μ‹œκ°„μ„ λ‚­λΉ„ ν•  κ²ƒμž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: AAA νŒ¨ν„΄μœΌλ‘œ κ΅¬μ„±λœ ν…ŒμŠ€νŠΈ

describe('고객 λΆ„λ₯˜κΈ°', () => {
    test('고객이 500λ‹¬λŸ¬ 이상을 μ†ŒλΉ„ν•œ 경우 ν”„λ¦¬λ―Έμ—„μœΌλ‘œ λΆ„λ₯˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.', () => {
        const customerToClassify = {spent:505, joined: new Date(), id:1}
        const DBStub = sinon.stub(dataAccess, "getCustomer")
            .reply({id:1, classification: 'regular'});

        const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);


πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: 뢄리 λ˜μ–΄μžˆμ§€ μ•Šκ³  ν•œ 벌둜 μž‘μ„±λ˜μ–΄ μžˆμ–΄ ν•΄μ„ν•˜κΈ° μ–΄λ ΅λ‹€.

test('ν”„λ¦¬λ―Έμ—„μœΌλ‘œ λΆ„λ₯˜ν•΄μ•Ό ν•©λ‹ˆλ‹€.', () => {
    const customerToClassify = {spent:505, joined: new Date(), id:1}
    const DBStub = sinon.stub(dataAccess, "getCustomer")
        .reply({id:1, classification: 'regular'});
    const receivedClassification = customerClassifier.classifyCustomer(customerToClassify);

βšͺ ️ 1.3 μ œν’ˆμ˜ μ–Έμ–΄λ‘œ μ˜ˆμƒκ°’μ„ μ„€λͺ…: BDD μŠ€νƒ€μΌμ˜ Assertion을 μ‚¬μš©

βœ… μ΄λ ‡κ²Œ 해라: ν…ŒμŠ€νŠΈλ₯Ό 선언적 μŠ€νƒ€μΌλ‘œ μž‘μ„±ν•˜λ©΄ μ½λŠ” μ‚¬λžŒμ΄ μ¦‰μ‹œ νŒŒμ•…ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 쑰건뢀 λ…Όλ¦¬λ‘œ μ±„μ›Œμ§„ λͺ…λ Ήν˜• μ½”λ“œλ‘œ μž‘μ„±ν•˜λ©΄ ν…ŒμŠ€νŠΈλ₯Ό 읽기가 쉽지 μ•ŠμŠ΅λ‹ˆλ‹€. 그런 μ˜λ―Έμ—μ„œ μž„μ˜μ˜ μ‚¬μš©μž μ •μ˜ μ½”λ“œλ₯Ό μ‚¬μš©ν•˜μ§€ 말고, 선언적 BDD μŠ€νƒ€μΌμ˜ expect λ˜λŠ” shouldλ₯Ό μ‚¬μš©ν•˜μ—¬ 인간과 같은 μ–Έμ–΄λ‘œ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜μ‹­μ‹œμ˜€. Chai & Jest에 μ›ν•˜λŠ” Assertion이 ν¬ν•¨λ˜μ–΄ μžˆμ§€ μ•Šκ³  λ°˜λ³΅μ„±μ΄ 높은 경우 extending Jest matcher (Jest) ν˜Ήμ€ custom Chai plugin μž‘μ„±μ„ κ³ λ €ν•˜μ‹­μ‹œμ˜€.

❌ 그렇지 μ•ŠμœΌλ©΄: νŒ€μ€ ν…ŒμŠ€νŠΈλ₯Ό 덜 μž‘μ„±ν•˜κ³  μ„±κ°€μ‹  것듀을 .skip() 으둜 μž₯μ‹ν•©λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: μ½λŠ” μ‚¬λžŒμ€ ν…ŒμŠ€νŠΈ μŠ€ν† λ¦¬λ₯Ό μ΄ν•΄ν•˜κΈ° μœ„ν•΄ μ§§μ§€μ•Šμ€ λͺ…λ Ήν˜• μ½”λ“œλ₯Ό 훑어봐야 ν•©λ‹ˆλ‹€.

test("κ΄€λ¦¬μž μš”μ²­μ΄ λ“€μ–΄μ˜€λ©΄ μ •λ ¬λœ κ΄€λ¦¬μž λͺ©λ‘λ§Œ 결과에 ν¬ν•¨λœλ‹€." , () => {
    // 여기에 두 λͺ…μ˜ κ΄€λ¦¬μž "admin1", "admin2" 및 "user1" 을 μΆ”κ°€ν–ˆλ‹€κ³  κ°€μ •ν•©λ‹ˆλ‹€.
    const allAdmins = getUsers({adminOnly:true});
    const admin1Found, adming2Found = false;
    allAdmins.forEach(aSingleUser => {
        if(aSingleUser === "user1"){
            assert.notEqual(aSingleUser, "user1", "κ΄€λ¦¬μžκ°€ μ•„λ‹Œ μ‚¬μš©μžλ₯Ό μ°Ύμ•˜λ‹€.");
            admin1Found = true;
            admin2Found = true;
    if(!admin1Found || !admin2Found ){
        throw new Error("λͺ¨λ“  κ΄€λ¦¬μžκ°€ λ°˜ν™˜λ˜μ§€ μ•Šμ•˜λ‹€.");

πŸ‘ μ˜¬λ°”λ₯Έ 예: λ‹€μŒκ³Ό 같은 선언적 ν…ŒμŠ€νŠΈλŠ” μ΄ν•΄ν•˜κΈ° μ‰½μŠ΅λ‹ˆλ‹€.

it("κ΄€λ¦¬μž μš”μ²­μ΄ λ“€μ–΄μ˜€λ©΄ μ •λ ¬λœ κ΄€λ¦¬μž λͺ©λ‘λ§Œ 결과에 ν¬ν•¨λœλ‹€." , () => {
    // 여기에 두 λͺ…μ˜ κ΄€λ¦¬μžλ₯Ό μΆ”κ°€ν–ˆλ‹€κ³  κ°€μ •ν•©λ‹ˆλ‹€.
    const allAdmins = getUsers({adminOnly:true});
    expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"])

βšͺ ️ 1.4 λΈ”λž™λ°•μŠ€ ν…ŒμŠ€νŠΈμ— μΆ©μ‹€: public method만 ν…ŒμŠ€νŠΈ

βœ… μ΄λ ‡κ²Œ 해라: λ‚΄λΆ€ν…ŒμŠ€νŠΈλŠ” 거의 아무것도 ν•˜μ§€ μ•ŠλŠ” μ—„μ²­λ‚œ μ˜€λ²„ν—€λ“œλ₯Ό λ°œμƒμ‹œν‚΅λ‹ˆλ‹€. λ§Œμ•½ λ‹Ήμ‹ μ˜ μ½”λ“œ ν˜Ήμ€ APIκ°€ μ˜¬λ°”λ₯Έ κ²°κ³Όλ₯Ό λ°˜ν™˜ν•œλ‹€λ©΄, λ‚΄λΆ€μ μœΌλ‘œ μ–΄λ–»κ²Œ λ™μž‘ν–ˆλŠ”μ§€μ˜ ν…ŒμŠ€νŠΈμ— 3μ‹œκ°„μ„ νˆ¬μžν•΄μ•Ό ν•©λ‹ˆκΉŒ? 깨지기 μ‰¬μš΄ ν…ŒμŠ€νŠΈλ₯Ό μœ μ§€ν•΄μ•Ό ν•©λ‹ˆκΉŒ? public methodκ°€ 잘 λ™μž‘ν•  λ•Œλ§ˆλ‹€ private method λ˜ν•œ μ•”μ‹œμ μœΌλ‘œ ν…ŒμŠ€νŠΈκ°€ 되고, νŠΉμ • 문제(예. 잘λͺ»λœ 좜λ ₯)κ°€ μžˆλŠ” κ²½μš°μ—λ§Œ ν…ŒμŠ€νŠΈκ°€ κΉ¨μ§‘λ‹ˆλ‹€. 이 접근법은 행동 ν…ŒμŠ€νŠΈλΌκ³ λ„ ν•©λ‹ˆλ‹€. λ‹€λ₯Έ ν•œνŽΈμœΌλ‘œ 당신은 λ‚΄λΆ€ ν…ŒμŠ€νŠΈλ₯Ό ν•΄μ•Όν•©λ‹ˆκΉŒ?(ν™”μ΄νŠΈλ°•μŠ€ μ ‘κ·Ό) - μ»΄ν¬λ„ŒνŠΈλ₯Ό μ„€κ³„ν•˜λŠ” κ²ƒμ—μ„œ 핡심 μ„ΈλΆ€ μ‚¬ν•­μœΌλ‘œ 초점이 μ΄λ™ν•˜κ±°λ‚˜ μž‘μ€ μ½”λ“œμ˜ λ¦¬νŽ™ν† λ§μœΌλ‘œ 인해 ν…ŒμŠ€νŠΈκ°€ 쀑단 될 수 μžˆμ§€λ§Œ, κ²°κ³ΌλŠ” ν›Œλ₯­ν•©λ‹ˆλ‹€. - μ΄λŠ” μœ μ§€λ³΄μˆ˜ 뢀담을 크게 μ¦κ°€μ‹œν‚΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈλŠ” λ‹€μŒκ³Ό 같이 λ™μž‘ν•©λ‹ˆλ‹€. μ–‘μΉ˜κΈ° μ†Œλ…„: λŠ‘λŒ€κ°€ λ‚˜νƒ€λ‚¬λ‹€!(예. private λ³€μˆ˜κ°€ λ³€κ²½λ˜μ–΄ ν…ŒμŠ€νŠΈμ— μ‹€νŒ¨ν•˜μ˜€μŠ΅λ‹ˆλ‹€). λ‹Ήμ—°νžˆ μ‚¬λžŒλ“€μ€, μ–Έμ  κ°€ μ§„μ§œ 버그가 λ¬΄μ‹œλ  λ•Œ κΉŒμ§€ CI μ•ŒλžŒμ„ λ¬΄μ‹œν•˜κΈ° μ‹œμž‘ν•  κ²ƒμž…λ‹ˆλ‹€...

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€λŠ” μ΄μœ μ—†μ΄ λ‚΄λΆ€λ₯Ό ν…ŒμŠ€νŠΈν•©λ‹ˆλ‹€.

class ProductService{
    // 이 method λŠ” λ‚΄λΆ€μ—μ„œλ§Œ μ‚¬μš©λ©λ‹ˆλ‹€.
    // 이 이름을 λ³€κ²½ν•˜λ©΄ ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•©λ‹ˆλ‹€.
        return {finalPrice: priceWithoutVAT * 1.2};
        // κ²°κ³Ό ν˜•μ‹μ΄λ‚˜ ν‚€ 이름을 λ³€κ²½ν•˜λ©΄ ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•©λ‹ˆλ‹€.
    // public method
        const desiredProduct= DB.getProduct(productId);
        finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice;

it("ν™”μ΄νŠΈλ°•μŠ€ ν…ŒμŠ€νŠΈ: λ‚΄λΆ€ methodκ°€ VAT 0을 λ°›μœΌλ©΄ 0을 λ°˜ν™˜ν•©λ‹ˆλ‹€.", async () => {
    // μ‚¬μš©μžκ°€ VATλ₯Ό 계산할 수 있게 ν•˜λŠ” μš”κ΅¬μ‚¬ν•­μ€ μ—†μœΌλ©°, μ΅œμ’… κ°€κ²©λ§Œ ν‘œμ‹œν•©λ‹ˆλ‹€.
    // κ·ΈλŸΌμ—λ„ λΆˆκ΅¬ν•˜κ³  μ—¬κΈ°μ—μ„œ λ‚΄λΆ€ ν…ŒμŠ€νŠΈ μˆ˜ν–‰
    expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0);

βšͺ ️ 1.5 μ˜¬λ°”λ₯Έ ν…ŒμŠ€νŠΈ 더블 선택: Stubκ³Ό Spyλ₯Ό μœ„ν•œ Mock을 ν”Όν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: ν…ŒμŠ€νŠΈ 더블은 μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ 내뢀에 μ—°κ²°λ˜μ–΄ μžˆκΈ°λ•Œλ¬Έμ— ν•„μš”μ•…μ΄μ§€λ§Œ μΌλΆ€λŠ” μ—„μ²­λ‚œ κ°€μΉ˜λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€(ν…ŒμŠ€νŠΈ 더블에 λŒ€ν•œ μ•Œλ¦Ό: mocks vs stubs vs spies).

ν…ŒμŠ€νŠΈ 더블을 μ‚¬μš©ν•˜κΈ° 전에 κ°„λ‹¨ν•œ 질문: μš”κ΅¬μ‚¬ν•­ λ¬Έμ„œμ— μžˆκ±°λ‚˜ μžˆμ„ 수 μžˆλŠ” κΈ°λŠ₯을 ν…ŒμŠ€νŠΈν•˜λŠ” 데 ν…ŒμŠ€νŠΈ 더블을 μ‚¬μš©ν•©λ‹ˆκΉŒ? λ§Œμ•½ μ•„λ‹ˆλΌλ©΄ ν™”μ΄νŠΈλ°•μŠ€ ν…ŒμŠ€νŠΈ λ‚Œμƒˆκ°€ λ³΄μž…λ‹ˆλ‹€.

예λ₯Ό λ“€μ–΄, 결제 μ„œλΉ„μŠ€κ°€ μ€‘λ‹¨λ˜μ—ˆμ„ λ•Œ 앱이 μ μ ˆν•˜κ²Œ μž‘λ™ν•˜λŠ” 것을 ν…ŒμŠ€νŠΈν•˜λ €λŠ” 경우, ν…ŒμŠ€νŠΈμ€‘μΈ λ‹¨μœ„κ°€ μ˜¬λ°”λ₯Έ 값을 λ°˜ν™˜ν•˜λ„λ‘, 결제 μ„œλΉ„μŠ€λ₯Ό stubν•˜κ³  '응닡 μ—†μŒ' λ°˜ν™˜μ„ 트리거 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이것은 νŠΉμ • μ‹œλ‚˜λ¦¬μ˜€μ—μ„œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λ™μž‘/응닡/κ²°κ³Όλ₯Ό ν™•μΈν•©λ‹ˆλ‹€. 그리고 spyλ₯Ό μ‚¬μš©ν•˜μ—¬ ν•΄λ‹Ή μ„œλΉ„μŠ€κ°€ μ€‘λ‹¨λ˜μ—ˆμ„ λ•Œ 메일이 λ³΄λ‚΄μ§€λŠ”μ§€λ₯Ό assert ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이것은 λ‹€μ‹œ μš”κ΅¬μ‚¬ν•­ λ¬Έμ„œμ— μžˆμ„ 수 μžˆλŠ” 행동에 λŒ€ν•œ μ κ²€μž…λ‹ˆλ‹€(κ²°μ œκ°€ μ €μž₯λ˜μ§€ μ•ŠμœΌλ©΄ 메일은 보낸닀). λ°˜λŒ€λ‘œ, 결제 μ„œλΉ„μŠ€λ₯Ό mock ν•˜κ³  μ˜¬λ°”λ₯Έ JavaScript νƒ€μž…μœΌλ‘œ 호좜 λ˜μ—ˆλŠ”μ§€λ₯Ό ν™•μΈν•œλ‹€λ©΄ - λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈλŠ” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ κΈ°λŠ₯에 μ „ν˜€ 영ν–₯을 받지 μ•Šκ³  자주 변경될 수 μžˆλŠ” λ‚΄λΆ€ κ΅¬ν˜„μ— μ΄ˆμ μ„ λ‘” κ²½μš°μž…λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: μ½”λ“œλ₯Ό λ¦¬νŽ™ν† λ§ ν•  λ•Œ, λͺ¨λ“  mock을 μ°Ύμ•„μ„œ μˆ˜μ •ν•΄μ•Ό ν•©λ‹ˆλ‹€. ν…ŒμŠ€νŠΈκ°€ 도움이 μ•„λ‹Œ 뢀담이 λ©λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: 내뢀에 μ΄ˆμ μ„ λ‘” mock

it("μœ νš¨ν•œ μ œν’ˆμ„ μ‚­μ œν•˜λ €κ³  ν•  λ•Œ, μ˜¬λ°”λ₯Έ μ œν’ˆκ³Ό μ˜¬λ°”λ₯Έ ꡬ성 μ •λ³΄λ‘œ 데이터 μ•‘μ„ΈμŠ€ DAL을 ν•œ 번 ν˜ΈμΆœν–ˆλŠ”μ§€ ν™•μΈν•œλ‹€", async () => {
    // 이미 μ œν’ˆμ„ μΆ”κ°€ν–ˆλ‹€κ³  κ°€μ •
    const dataAccessMock = sinon.mοΏ½ock(DAL);
    // 쒋지 μ•ŠμŒ: λ‚΄λΆ€ ν…ŒμŠ€νŠΈλŠ” side-effectλ₯Ό μœ„ν•΄μ„œκ°€ μ£Όμš” λͺ©μ μ„ μœ„ν•΄μ„œ μž…λ‹ˆλ‹€.
    dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false);
    new ProductService().deletePrice(theProductWeJustAdded);

πŸ‘μ˜¬λ°”λ₯Έ 예: spyλŠ” μš”κ΅¬μ‚¬ν•­μ„ ν…ŒμŠ€νŠΈν•˜λŠ”λ° μ΄ˆμ μ„ λ‘κ³ μžˆμ§€λ§Œ, λ‚΄λΆ€λ₯Ό κ±΄λ“œλ¦¬λŠ” side-effectλ₯Ό ν”Όν•  순 μ—†μŠ΅λ‹ˆλ‹€.

it("μœ νš¨ν•œ μ œν’ˆμ„ μ‚­μ œν•˜λ €κ³  ν•  λ•Œ, 메일을 보낸닀", async () => {
    // 이미 μ œν’ˆμ„ μΆ”κ°€ν–ˆλ‹€κ³  κ°€μ •
    const spy = sinon.spy(Emailer.prototype, "sendEmail");
    new ProductService().deletePrice(theProductWeJustAdded);
    // μ’‹μŒ: μš°λ¦¬λŠ” λ‚΄λΆ€λ₯Ό λ‹€λ£¨λŠ”κ°€? κ·Έλ ‡λ‹€, κ·ΈλŸ¬λ‚˜ μš”κ΅¬μ‚¬ν•­(이메일을 보낸닀)에 λŒ€ν•œ ν…ŒμŠ€νŠΈμ˜ side-effect이닀.

βšͺ ️ 1.6 μ˜λ―Έμ—†λŠ” 인풋 데이터λ₯Ό μ‚¬μš©ν•˜μ§€ 말고, μ‹€μ œμ™€ 같은 인풋 데이터λ₯Ό μ‚¬μš©ν•΄λΌ

βœ… μ΄λ ‡κ²Œ 해라: ν”νžˆ μ œν’ˆμ˜ 버그듀은 맀우 νŠΉμˆ˜ν•œ 인풋데이터λ₯Ό 톡해 λ‚˜νƒ€λ‚©λ‹ˆλ‹€ - ν…ŒμŠ€νŠΈ 인풋이 ν˜ˆμ‹€μ μΌ 수둝 버그λ₯Ό 쑰기에 λ°œκ²¬ν•  κ°€λŠ₯성이 λ†’μ•„μ§‘λ‹ˆλ‹€. μ‹€μ œ 데이터와 λ‹€μ–‘μ„± 및 ν˜•νƒœκ°€ μœ μ‚¬ν•œ 데이터λ₯Ό 생성해 μ£ΌλŠ” Faker 같은 μ „μš© λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ„ μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. 이런 λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ€ μ‹€μ œκ°™μ€ μ „ν™”λ²ˆν˜Έ, μ‚¬μš©μž 이름, μ‹ μš©μΉ΄λ“œ, νšŒμ‚¬λͺ… 그리고 심지어 'lorem ipsum'같은 λ¬Έμžλ“±μ„ 생성할 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. 당신은 κ°€μƒμ˜ 데이터λ₯Ό μ‚¬μš©ν•˜μ—¬ ν…ŒμŠ€νŠΈ(λ‹¨μœ„ ν…ŒμŠ€νŠΈ μœ„μ—μ„œ)λ₯Ό λ¬΄μž‘μœ„ν™” ν•˜κ±°λ‚˜ 심지어 μ‹€μ œ ν™˜κ²½μœΌλ‘œλΆ€ν„°μ˜ μ‹€μ œ 데이터λ₯Ό μž„ν¬νŠΈ ν• μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. λ‹€μŒ 단계λ₯Ό μ–»κΈ°λ₯Ό μ›ν•˜μ‹­λ‹ˆκΉŒ? κ·Έλ ‡λ‹€λ©΄ μ•„λž˜λ‘œ κ°€μ‹­μ‹œμ˜€ (property-based testing).

❌ 그렇지 μ•Šλ‹€λ©΄: "Foo"와 같은 인풋을 μ‚¬μš©ν•˜λ©΄ λ‹Ήμ‹ μ˜ λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ λͺ¨λ‘ ν†΅κ³Όν•œκ²ƒ 처럼 ν‘œμ‹œλ˜μ§€λ§Œ, μ‹€μ œ ν™˜κ²½μ—μ„œλŠ” 해컀가 β€œ@3e2ddsf . ##’ 1 fdsfds . fds432 AAAA” 같은 인풋을 전달해 μ‹€νŒ¨ ν• μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: ν˜„μ‹€μ μ΄μ§€ μ•Šμ€ 데이터 λ•Œλ¬Έμ— ν†΅κ³Όν•˜λŠ” ν…ŒμŠ€νŠΈ

const addProduct = (name, price) =>{
  const productNameRegexNoSpace = /^\S*$/;// 곡백은 ν—ˆμš©λ˜μ§€ μ•ŠμŒ

    return false;//λ„λ‹¬ν•˜μ§€ μ•ŠλŠ” κ³³

    //some logic here
    return true;

test("잘λͺ»λœ 예제: μœ νš¨ν•œ 속성과 ν•¨κ»˜ μ œν’ˆμ„ μΆ”κ°€ν•œλ‹€λ©΄, 성곡을 μ–»λŠ”λ‹€.", async () => {
    //λͺ¨λ“  ν…ŒμŠ€νŠΈμ—μ„œ false κ°€ λ¦¬ν„΄λ˜μ§€ μ•ŠλŠ” "Foo" 인풋을 μ‚¬μš©
    const addProductResult = addProduct("Foo", 5);
    //κ±°μ§“λœ 성곡: 곡백을 ν¬ν•¨ν•˜λŠ” λ¬Έμžμ—΄μ„ μ‚¬μš©ν•˜μ§€ μ•Šμ•˜κΈ° λ•Œλ¬Έμ— ν…ŒμŠ€νŠΈλŠ” μ„±κ³΅ν•œλ‹€.

πŸ‘μ˜¬λ°”λ₯Έ 예: λ¬΄μž‘μœ„ν•œ ν˜„μ‹€μ μΈ 인풋

it("더 λ‚˜μ€ 것: μœ νš¨ν•œ μ œν’ˆμ΄ μΆ”κ°€λœλ‹€λ©΄, 성곡을 μ–»λŠ”λ‹€.", async () => {
    const addProductResult = addProduct(faker.commerce.productName(), faker.random.number());
    //μƒμ„±λœ λ¬΄μž‘μœ„ 인풋: {'Sleek Cotton Computer',  85481}
    //ν…ŒμŠ€νŠΈλŠ” μ‹€νŒ¨ν•œλ‹€, λ¬΄μž‘μœ„ 인풋은 μš°λ¦¬κ°€ κ³„νšν•˜μ§€ μ•Šμ€ 일이 μΌμ–΄λ‚˜λ„λ‘ λ§Œλ“ λ‹€.
    //μš°λ¦¬λŠ” 쑰기에 버그λ₯Ό λ°œκ²¬ν–ˆλ‹€!

βšͺ ️ 1.7 Β ν”„λ‘œνΌν‹° 기반(Property-based) ν…ŒμŠ€νŠΈλ₯Ό 톡해 λ‹€μ–‘ν•œ 인풋 κ°’ μ‘°ν•©μœΌλ‘œ ν…ŒμŠ€νŠΈλ₯Ό ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: μš°λ¦¬λŠ” 일반적으둜 적은 수의 인풋 μƒ˜ν”Œ 데이터λ₯Ό 가지고 ν…ŒμŠ€νŠΈλ₯Ό ν•©λ‹ˆλ‹€. 심지어 인풋 데이터 ν˜•μ‹μ΄ μ‹€μ œ 데이터와 λΉ„μŠ·ν•  λ•Œμ—λ„ λ‹€μŒκ³Ό 같이 μ œν•œλœ 인풋 μ‘°ν•©μœΌλ‘œλ§Œ ν…ŒμŠ€νŠΈλ₯Ό μ»€λ²„ν•©λ‹ˆλ‹€.(method(β€˜β€™, true, 1), method(β€œstring” , false” , 0)) ν•˜μ§€λ§Œ, μš΄μ˜μ‹œμ—λŠ” 5개의 νŒŒλΌλ―Έν„°λ₯Ό κ°€μ§€λŠ” APIλŠ” 수 천 개의 λ‹€λ₯Έ μ‘°ν•©μ˜ νŒŒλΌλ―Έν„°λ‘œ 호좜 될 수 있고, 이 쀑 ν•˜λ‚˜κ°€ 우리의 μ‹œμŠ€ν…œμ„ λ‹€μš΄μ‹œν‚¬ μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. κ·Έλ ‡λ‹€λ©΄ λ§Œμ•½ 1000 가지 μ‘°ν•©μ˜ 인풋값을 μžλ™μœΌλ‘œ μƒμ„±ν•˜κ³  μ˜¬λ°”λ₯Έ 응닡을 λ°˜ν™˜ν•˜μ§€ λͺ»ν•˜λŠ” 인풋값을 μ°Ύμ•„λ‚΄λŠ” λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•  수 μžˆλ‹€λ©΄ μ–΄λ–¨κΉŒμš”? ν”„λ‘œνΌν‹° 기반 ν…ŒμŠ€νŠΈλŠ” λ‹¨μœ„ ν…ŒμŠ€νŠΈμ— λͺ¨λ“  κ°€λŠ₯ν•œ 인풋 쑰합을 μ‚¬μš©ν•˜μ—¬ μƒκ°ν•˜μ§€ λͺ» ν•œ 버그λ₯Ό 찾을 ν™•λ₯ μ„ λ†’μ—¬μ€λ‹ˆλ‹€. 예λ₯Όλ“€μ–΄, λ‹€μŒμ˜ λ©”μ†Œλ“œκ°€ μ£Όμ–΄μ‘Œμ„ λ•Œ β€”β€ŠaddNewProduct(id, name, isDiscount)β€Šβ€” ν”„λ‘œνΌν‹° 기반 ν…ŒμŠ€νŠΈ λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ€ λ‹€μ–‘ν•œ νŒŒλΌλ―Έν„° (number, string, boolean) μ‘°ν•©μœΌλ‘œ - (1, β€œiPhone”, false), (2, β€œGalaxy”, true) - 이 λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•©λ‹ˆλ‹€. js-verify λ‚˜ testcheck (much better documentation) 같은 라이브러리λ₯Ό μ§€μ›ν•˜λŠ” ν…ŒμŠ€νŠΈ λŸ¬λ„ˆλ“€ (Mocha, Jest, etc) 쀑 당신이 κ°€μž₯ μ„ ν˜Έν•˜λŠ” 방법을 톡해 ν”„λ‘œνΌν‹° 기반 ν…ŒμŠ€νŠΈλ₯Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ—…λ°μ΄νŠΈ : Nicolas Dubienκ°€ μ½”λ©˜νŠΈλ₯Ό 톡해 더 λ§Žμ€ 뢀가적인 κΈ°λŠ₯듀을 μ œκ³΅ν•˜κ³  ν™œλ°œν•˜κ²Œ μœ μ§€λ³΄μˆ˜λ˜κ³  μžˆλŠ” 라이브러리 fast-checkλ₯Ό μΆ”μ²œν•΄ μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: μ˜μ‹¬ν•  여지 없이 당신은 였직 μ½”λ“œκ°€ 잘 λ™μž‘ν•˜λŠ” ν…ŒμŠ€νŠΈ 인풋을 μ‚¬μš©ν•  κ²ƒμž…λ‹ˆλ‹€. λΆˆν–‰ν•˜κ²Œλ„ μ΄λŸ¬ν•œ 방식은 버그λ₯Ό μ°ΎλŠ” λ„κ΅¬λ‘œμ¨μ˜ ν…ŒμŠ€νŠΈ νš¨μœ¨μ„±μ„ λ–¨μ–΄λœ¨λ¦΄ 것 μž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: β€œfast-check”λ₯Ό μ‚¬μš©ν•˜μ—¬ λ‹€μ–‘ν•œ 인풋 μ‘°ν•©μœΌλ‘œ ν…ŒμŠ€νŠΈ ν•˜μ‹­μ‹œμ˜€.

import fc from "fast-check";

describe("Product service", () => {
  describe("Adding new", () => {
    //μ„œλ‘œ λ‹€λ₯Έ λ¬΄μž‘μœ„ κ°’μœΌλ‘œ 100회 ν˜ΈμΆœλ©λ‹ˆλ‹€.
    it("Add new product with random yet valid properties, always successful", () =>
      fc.assert(, fc.string(), (id, name) => {
          expect(addNewProduct(id, name).status).toEqual("approved");

βšͺ ️ 1.8 ν•„μš”ν•œ 경우 μ§§κ±°λ‚˜ 인라인 μŠ€λƒ…μƒ·λ§Œ μ‚¬μš©ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: μŠ€λƒ…μƒ· ν…ŒμŠ€νŠΈκ°€ ν•„μš”ν•œ 경우 μ™ΈλΆ€ 파일이 μ•„λ‹Œ ν…ŒμŠ€νŠΈμ˜ 일뢀 (인라인 μŠ€λƒ…μƒ·)에 포함 된 짧고 μ§‘μ€‘λœ μŠ€λƒ…μƒ·(3~7 라인)만 μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. 이 지침을 λ”°λ₯΄λ©΄ λ”°λ‘œ μ„€λͺ…이 ν•„μš”μ—†κ³  잘 깨지지 μ•ŠλŠ” ν…ŒμŠ€νŠΈκ°€ λ©λ‹ˆλ‹€.

λ°˜λ©΄μ—, '고전적인 μŠ€λƒ…μƒ·' νŠœν† λ¦¬μ–Ό 및 λ„κ΅¬λŠ” 외뢀에 큰 파일(예: ꡬ성 μš”μ†Œ λžœλ”λ§ λ§ˆν¬μ—…, API JSON κ²°κ³Ό)λ₯Ό μ €μž₯ν•˜κ³ , ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•  λ•Œ λ§ˆλ‹€ μˆ˜μ‹ λœ κ²°κ³Όλ₯Ό μ €μž₯된 버전과 λΉ„κ΅ν•˜κΈ°λ₯Ό ꢌμž₯ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 이것은 1,000 라인(μš°λ¦¬κ°€ μ ˆλŒ€ 읽지 μ•Šκ³  μΆ”λ‘ ν•˜μ§€ μ•Šμ„ 3,000개의 데이터 값을 가진)의 μ½”λ“œλ₯Ό 우리 ν…ŒμŠ€νŠΈμ— μ•”μ‹œμ μœΌλ‘œ μ—°κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ™œ 이것이 잘λͺ» λ˜μ—ˆμ„κΉŒμš”? μ΄λ ‡κ²Œν•˜λ©΄ ν…ŒμŠ€νŠΈμ— μ‹€νŒ¨ν•  1,000 가지 μ΄μœ κ°€ μƒκΉλ‹ˆλ‹€.οΏ½ ν•œμ€„λ§Œ λ³€κ²½λ˜μ–΄λ„ οΏ½μŠ€λƒ…μƒ·μ΄ μœ νš¨ν•˜μ§€ μ•Šκ²Œ 되고, 이런일이 일어날 κ°€λŠ₯성이 λ†’μŠ΅λ‹ˆλ‹€. μ–Όλ§ˆλ‚˜ 자주? λͺ¨λ“  곡백, μ£Όμ„μ—μ„œ ν˜Ήμ€ μ‚¬μ†Œν•œ CSS/HTML 변경에 λŒ€ν•΄μ„œ. 뿐만 μ•„λ‹ˆλΌ ν…ŒμŠ€νŠΈ 이름은 1,000 라인이 λ³€κ²½λ˜μ§€ μ•Šμ•˜λŠ”μ§€λ₯Ό λ‚˜νƒ€λ‚΄κΈ° λ•ŒλΆ„μ—, μ‹€νŒ¨οΏ½μ— λŒ€ν•œ λ‹¨μ„œλ₯Ό μ œκ³΅ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λ˜ν•œ ν…ŒμŠ€νŠΈ μž‘μ„±μžκ°€ κΈ΄ λ¬Έμ„œ(κ²€μ‚¬ν•˜κ³  확인할 수 μ—†λŠ”)λ₯Ό λ°›μ•„λ“€μ΄κ²Œλ” ν•©λ‹ˆλ‹€. 이 λͺ¨λ“  것은 초점이 λ§žμ§€μ•Šκ³  λ„ˆλ¬΄ λ§Žμ€ 것을 λ‹¬μ„±ν•˜λ €λŠ” λͺ¨ν˜Έν•˜κ³  κ°„μ ˆν•œ ν…ŒμŠ€νŠΈ μ¦μƒμž…λ‹ˆλ‹€.

κΈ΄ μ™ΈλΆ€ μŠ€λƒ…μƒ·μ΄ ν—ˆμš©λ˜λŠ” κ²½μš°κ°€ 거의 μ—†λ‹€λŠ” 점은 μ£Όλͺ©ν•  κ°€μΉ˜κ°€ μžˆμŠ΅λ‹ˆλ‹€ - 데이터가 μ•„λ‹Œ μŠ€ν‚€λ§ˆλ₯Ό assert ν•  λ•Œ(κ°’ μΆ”μΆœ 및 ν•„λ“œμ— 집쀑) λ˜λŠ” μˆ˜μ‹ λœ λ¬Έμ„œκ°€ 거의 λ³€κ²½λ˜μ§€ μ•ŠλŠ” 경우

❌ 그렇지 μ•Šλ‹€λ©΄: UI ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•©λ‹ˆλ‹€. μ½”λ“œκ°€ λ¬Έμ œμ—†μ–΄ 보이고 화면이 μ™„λ²½ν•œ 픽셀을 λ Œλ”λ§ν•©λ‹ˆλ‹€. μ–΄λ–»κ²Œ λ˜μ—ˆμŠ΅λ‹ˆκΉŒ? μŠ€λƒ…μƒ· ν…ŒμŠ€νŠΈμ—μ„œ 원본 λ¬Έμ„œμ™€ ν˜„μž¬ μˆ˜μ‹ λœ λ¬Έμ„œμ™€μ˜ 차이점을 λ°œκ²¬ν–ˆμŠ΅λ‹ˆλ‹€. 빈칸 ν•˜λ‚˜κ°€ 마크 λ‹€μš΄μ— μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€...

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: 보이지 μ•ŠλŠ” 2,000 라인의 μ½”λ“œλ₯Ό 우리 ν…ŒμŠ€νŠΈμ— μ—°κ²°

it(' 이 μ˜¬λ°”λ₯΄κ²Œ λžœλ”λ§ λœλ‹€.', ()  => {


const receivedPage = renderer
.create(  <DisplayPage page  =  ""  > Test JavaScript < /DisplayPage>)

// 이제 2,000 라인의 λ¬Έμ„œλ₯Ό μ•”λ¬΅μ μœΌλ‘œ μœ μ§€ν•©λ‹ˆλ‹€.
// λͺ¨λ“  μ€„λ°”κΏˆ λ˜λŠ” 주석이 ν…ŒμŠ€νŠΈλ₯Ό λ§κ°€λœ¨λ¦½λ‹ˆλ‹€.


πŸ‘ μ˜¬λ°”λ₯Έ 예: expectation이 잘 보이고 μ§‘μ€‘λœλ‹€.

it(' ν™ˆνŽ˜μ΄μ§€λ₯Ό λ°©λ¬Έν•˜λ©΄ 메뉴가 보인닀.', () => {

receivedPage tree = renderer
.create(  <DisplayPage page  =  ""  > Test JavaScript < /DisplayPage>)


const menu =;
<li> About </li>
<li> Contact </li>

βšͺ ️ 1.9 ν…ŒμŠ€νŠΈ 데이터λ₯Ό κΈ€λ‘œλ²Œλ‘œ ν•˜μ§€λ§κ³  ν…ŒμŠ€νŠΈλ³„λ‘œ λ”°λ‘œ μΆ”κ°€ν•˜λΌ.

βœ… μ΄λ ‡κ²Œ 해라: ν™©κΈˆλ₯ μ— λ”°λ₯΄λ©΄(μ„Ήμ…˜ 0), 각 ν…ŒμŠ€νŠΈλŠ” μ»€ν”Œλ§μ„ λ°©μ§€ν•˜κ³  ν…ŒμŠ€νŠΈ 흐름을 μ‰½κ²Œ μΆ”λ‘ ν•˜κΈ° μœ„ν•΄ 자체 DB 데이터λ₯Ό μΆ”κ°€ν•˜κ³  μ‹€ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€. μ‹€μ œλ‘œ μ„±λŠ₯ ν–₯상(ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜κΈ° 전에 DB 데이터λ₯Ό μ€€λΉ„('ν…ŒμŠ€νŠΈ ν”½μŠ€μ³'라고도 ν•©λ‹ˆλ‹€))을 μœ„ν•΄ 이λ₯Ό μœ„λ°˜ν•˜λŠ” ν…ŒμŠ€ν„°λ“€μ΄ λ§ŽμŠ΅λ‹ˆλ‹€. μ„±λŠ₯은 μ‹€μ œλ‘œ μœ νš¨ν•œ λ¬Έμ œμ΄μ§€λ§Œ 완화될 수 μžˆμŠ΅λ‹ˆλ‹€(2.2 μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈ μ°Έκ³ ). κ·ΈλŸ¬λ‚˜ ν…ŒμŠ€νŠΈ λ³΅μž‘μ„±μ€ λŒ€λΆ€λΆ„μ˜ λ‹€λ₯Έ 고렀사항듀을 ν†΅μ œν•΄μ•Ό ν•˜λŠ” 고톡을 μˆ˜λ°˜ν•©λ‹ˆλ‹€. 각 ν…ŒμŠ€νŠΈμ— ν•„μš”ν•œ DB λ ˆμ½”λ“œλ₯Ό λͺ…μ‹œμ μœΌλ‘œ μΆ”κ°€ν•˜κ³ , ν•΄λ‹Ή 데이터에 λŒ€ν•΄μ„œλ§Œ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜μ‹­μ‹œμ˜€. μ„±λŠ₯이 μ€‘μš”ν•œ λ¬Έμ œκ°€ λ˜λŠ” 경우 - 데이터λ₯Ό λ³€κ²½ν•˜μ§€ μ•ŠλŠ” ν…ŒμŠ€νŠΈ λͺ¨μŒ(예: 쿼리)에 λŒ€ν•΄μ„œ 데이터λ₯Ό μ€€λΉ„ν•˜λŠ” ν˜•νƒœλ‘œ νƒ€ν˜‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: ν…ŒμŠ€νŠΈ μ‹€νŒ¨, 배포 μ€‘λ‹¨μœΌλ‘œ νŒ€μ›λ“€μ΄ κ·€μ€‘ν•œ μ‹œκ°„μ„ μ†ŒλΉ„ν•  κ²ƒμž…λ‹ˆλ‹€. 버그가 μžˆμŠ΅λ‹ˆκΉŒ? μ‘°μ‚¬ν•΄λ³΄λ‹ˆ 'μ—†μŠ΅λ‹ˆλ‹€' - 두 ν…ŒμŠ€νŠΈμ—μ„œ λ™μΌν•œ ν…ŒμŠ€νŠΈ 데이터λ₯Ό λ³€κ²¨μ•ˆ κ²ƒμœΌλ‘œ λ³΄μž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: ν…ŒμŠ€νŠΈλŠ” 독립적이지 μ•ŠμœΌλ©° κΈ€λ‘œλ²Œ 훅에 μ˜ν•œ DB 데이터에 의쑴

before(() => {
  // μ‚¬μ΄νŠΈ 및 κ΄€λ¦¬μž 데이터λ₯Ό DB에 μΆ”κ°€. λ°μ΄ν„°λŠ” 어디에 μžˆμŠ΅λ‹ˆκΉŒ? 외뢀에. μ™ΈλΆ€ JSON λ˜λŠ” λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ ν”„λ ˆμž„μ›Œν¬μ—
  await DB.AddSeedDataFromJson('seed.json');
it("μ‚¬μ΄νŠΈ 이름을 μ—…λ°μ΄νŠΈ ν•  λ•Œ, 성곡을 ν™•μΈν•œλ‹€.", async () => {
  // μ‚¬μ΄νŠΈ 이름 "portal"이 μ‘΄μž¬ν•œλ‹€λŠ” 것을 μ•Œκ³ μžˆμŠ΅λ‹ˆλ‹€. μ‹œλ“œνŒŒμΌμ—μ„œ λ΄€μŠ΅λ‹ˆλ‹€.
  const siteToUpdate = await SiteService.getSiteByName("Portal");
  const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
it("μ‚¬μ΄νŠΈ 이름을 쿼리할 λ•Œ, μ˜¬λ°”λ₯Έ μ‚¬μ΄νŠΈ 이름을 μ–»λŠ”λ‹€.", async () => {
  // μ‚¬μ΄νŠΈ 이름 "portal"이 μ‘΄μž¬ν•œλ‹€λŠ” 것을 μ•Œκ³ μžˆμŠ΅λ‹ˆλ‹€. μ‹œλ“œνŒŒμΌμ—μ„œ λ΄€μŠ΅λ‹ˆλ‹€.
  const siteToCheck = await SiteService.getSiteByName("Portal");
  expect("Portal"); // μ‹€νŒ¨! 이전 ν…ŒμŠ€νŠΈμ—μ„œ 이름이 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€. γ… γ… 

πŸ‘ μ˜¬λ°”λ₯Έ 예: μš°λ¦¬λŠ” ν…ŒμŠ€νŠΈ λ‚΄λΆ€μ—λ§Œ λ¨Έλ¬Ό 수 있으며, 각 ν…ŒμŠ€νŠΈλŠ” 자체 데이터 μ„ΈνŠΈμ—μ„œ λ™μž‘ν•©λ‹ˆλ‹€.

it("μ‚¬μ΄νŠΈ 이름을 μ—…λ°μ΄νŠΈ ν•  λ•Œ, 성곡을 ν™•μΈν•œλ‹€.", async () => {
  // ν…ŒμŠ€νŠΈλŠ” μƒˆλ‘œμš΄ λ ˆμ½”λ“œλ₯Ό μƒˆλ‘œ μΆ”κ°€ν•˜κ³  ν•΄λ‹Ή λ ˆμ½”λ“œμ— λŒ€ν•΄μ„œλ§Œ λ™μž‘ν•©λ‹ˆλ‹€.
  const siteUnderTest = await SiteService.addSite({
    name: "siteForUpdateTest"
  const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");

βšͺ ️ 1.10 였λ₯˜λ₯Ό catch ν•˜μ§€λ§κ³  expect ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: 였λ₯˜λ₯Ό λ°œμƒμ‹œν‚€λŠ” μž…λ ₯값을 assert ν•  λ•Œ, try-catch-finallyλ₯Ό μ‚¬μš©ν•˜κ³  catch λΈ”λŸ­μ—μ„œ assert ν•˜λŠ”κ²Œ λ§žμ•„ λ³΄μΌμˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. μ•„λž˜ μ˜ˆλŠ” ν…ŒμŠ€νŠΈ μ˜λ„μ™€ κ²°κ³Ό expectation을 μˆ¨κΈ°λŠ” μ–΄μƒ‰ν•˜κ³  μž₯ν™©ν•œ ν…ŒμŠ€νŠΈ μ‚¬λ‘€μž…λ‹ˆλ‹€.

보닀 μš°μ•„ν•œ λŒ€μ•ˆμ€ ν•œμ€„μ§œλ¦¬ Chai assertion을 μ‚¬μš©ν•˜λŠ” 것 μž…λ‹ˆλ‹€: expect(method).to.throw (ν˜Ήμ€ Jest: expect(method).toThrow()). 였λ₯˜ μœ ν˜•μ„ μ•Œλ €μ£ΌλŠ” 속성이 μ˜ˆμ™Έμ— ν¬ν•¨λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. 그렇지 μ•Šκ³  일반적인 였λ₯˜λ₯Ό λ°œμƒμ‹œν‚€λ©΄ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ€ μ‚¬μš©μžμ—κ²Œ μ‹€λ§μŠ€λŸ¬μš΄ λ©”μ‹œμ§€λ₯Ό ν‘œμ‹œν•˜λŠ” 것 밖에 ν•  수 μ—†μŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: 무엇이 잘λͺ»λ˜μ—ˆλŠ”지 ν…ŒμŠ€νŠΈ λ³΄κ³ μ„œ(예: CI λ³΄κ³ μ„œ)μ—μ„œ μΆ”λ‘ ν•˜κΈ° μ–΄λ €μšΈ κ²ƒμž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: try-catch둜 였λ₯˜κ°€ μ‘΄μž¬ν•œλ‹€κ³  assert ν•˜λŠ” κΈ΄ ν…ŒμŠ€νŠΈ 사둀

it("μ œν’ˆλͺ…이 μ—†μœΌλ©΄ 400 였λ₯˜λ₯Ό λ˜μ§„λ‹€.", async() => {
  let errorWeExceptFor = null;
  try {
    const result = await addNewProduct({name:'nest'});}
  catch (error) {
    errorWeExceptFor = error;
  // 이 asserting이 μ‹€νŒ¨ν•˜λ©΄, ν…ŒμŠ€νŠΈ κ²°κ³Όμ—μ„œ λˆ„λ½λœ μž…λ ₯값에 λŒ€ν•œ λ‹¨μ–΄λŠ” μ•Œ 수 μ—†κ³ 
  // μž…λ ₯값이 null μ΄λΌλŠ” κ²ƒλ§Œ μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€.

πŸ‘ μ˜¬λ°”λ₯Έ 예: QAλ‚˜ PM이라도 μ‰½κ²Œ 이해할 수 있고 읽기 μ‰¬μš΄ expectation

it.only("μ œν’ˆλͺ…이 μ—†μœΌλ©΄ 400 였λ₯˜λ₯Ό λ˜μ§„λ‹€.", async() => {
  expect(addNewProduct)).to.eventually.throw(AppError)'code', "InvalidInput");

βšͺ ️ 1.11 ν…ŒμŠ€νŠΈμ— νƒœκΉ…ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: λ‹€λ₯Έ ν…ŒμŠ€νŠΈλŠ” κΌ­ λ‹€λ₯Έ μ‹œλ‚˜λ¦¬μ˜€μ—μ„œ μ‹€ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€: κ°œλ°œμžκ°€ νŒŒμΌμ„ μ €μž₯ν•˜κ±°λ‚˜ 컀밋을 ν•  λ•Œ λΉ λ₯΄κ³ , IOκ°€ 많이 μ—†λŠ” ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€. 전체 end-to-end ν…ŒμŠ€νŠΈλŠ” 일반적으둜 μƒˆλ‘œμš΄ Pull Requestκ°€ μ œμΆœλ˜μ—ˆμ„ λ•Œ μ‹€ν–‰λ©λ‹ˆλ‹€. λ“±.. μ΄λŸ¬ν•œ κ²½μš°μ— #cold #api #sanity와 같은 ν‚€μ›Œλ“œλ‘œ ν…ŒμŠ€νŠΈμ— νƒœκΉ…ν•˜λ©΄ ν…ŒμŠ€νŠΈλ₯Ό 효율적으둜 grep ν•  수 있고, μ›ν•˜λŠ” ν•˜μœ„μ„ΈνŠΈλ₯Ό ν˜ΈμΆœν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예) Mochaλ₯Ό μ΄μš©ν•΄μ„œ sanity ν…ŒμŠ€νŠΈ 그룹만 μ‹€ν–‰ν•˜λŠ” λ°©λ²•μž…λ‹ˆλ‹€: mocha - grep 'sanity'

❌ 그렇지 μ•ŸμœΌλ©΄: κ°œλ°œμžκ°€ μž‘μ€ 변경을 ν•  λ•Œλ§ˆλ‹€ μˆ˜μ‹­ 개의 DB 쿼리λ₯Ό μˆ˜ν–‰ν•˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό ν¬ν•¨ν•œ λͺ¨λ“  ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•œλ‹€λ©΄, 속도가 맀우 느렀져 κ°œλ°œμžκ°€ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜μ§€ μ•Šκ²Œ λ§Œλ“€ κ²ƒμž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: ν…ŒμŠ€νŠΈλ₯Ό '#cold-test'둜 νƒœκΉ…ν•˜λ©΄ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜λŠ” μ‚¬λžŒμ΄ λΉ λ₯Έ ν…ŒμŠ€νŠΈλ§Œ μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€(IOλ₯Ό μˆ˜ν–‰ν•˜μ§€ μ•Šκ³  κ°œλ°œμžκ°€ μ½”λ”©ν•˜λŠ” 쀑에도 자주 μ‹€ν–‰ν•  수 μžˆλŠ” ν…ŒμŠ€νŠΈ cold === quick).

// 이 ν…ŒμŠ€νŠΈλŠ” λΉ λ₯΄κ³ (DB μ—†μŒ) ν˜„μž¬ μ‚¬μš©μž/CIκ°€ 자주 μ‹€ν–‰ν•  수 μžˆλŠ” νƒœκ·Έλ₯Ό μ§€μ •ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.
describe('μ£Όλ¬Έ μ„œλΉ„μŠ€', function() {
  describe('μƒˆ μ£Όλ¬Έ μΆ”κ°€ #cold-test #sanity', function() {
    test('μ‹œλ‚˜λ¦¬μ˜€ - 톡화가 μ œκ³΅λ˜μ§€ μ•ŠμŒ. μ˜ˆμ™Έ - κΈ°λ³Έ 톡화 μ‚¬μš© #sanity', function() {
      // code logic here

βšͺ ️ 1.12 일반적인 쒋은 ν…ŒμŠ€νŠΈ 기법듀

οΏ½ βœ… μ΄λ ‡κ²Œ 해라: 이 글은 Node.js와 관련이 μžˆκ±°λ‚˜ μ΅œμ†Œν•œ Node.js둜 예λ₯Ό λ“€ 수 μžˆλŠ” ν…ŒμŠ€νŠΈ 쑰언에 쀑점을두고 μžˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ΄λ²ˆμ—λŠ” Node.jsκ°€ μ•„λ‹ˆμ§€λ§Œ 잘 μ•Œλ €μ§„ νŒλ“€μ„ ν¬ν•¨ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€.

TDD 원칙을 배우고 μ—°μŠ΅ν•˜μ‹­μ‹œμ˜€ - λ§Žμ€ μ‚¬λžŒλ“€μ—κ²Œ 맀우 κ°€μΉ˜κ°€ μžˆμ§€λ§Œ, μžμ‹ μ˜ μŠ€νƒ€μΌμ— λ§žμ§€ μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. μ‹€νŒ¨-성곡-λ¦¬νŽ˜ν† λ§ μŠ€νƒ€μΌλ‘œ μ½”λ“œ μž‘μ„± 전에 ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λŠ” 것을 κ³ λ €ν•˜μ‹­μ‹œμ˜€. 버그λ₯Ό λ°œκ²¬ν•˜λ©΄ 각 ν…ŒμŠ€νŠΈμ—μ„œ μ •ν™•νžˆ ν•œ κ°€μ§€λ§Œ ν™•μΈν•˜λ„λ‘ ν•˜μ‹­μ‹œμ˜€. μˆ˜μ •ν•˜κΈ° 전에 μ•žμœΌλ‘œ 이 버그λ₯Ό 발견 ν•  ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜μ‹­μ‹œμ˜€. ν…ŒμŠ€νŠΈκ°€ μ„±κ³΅ν•˜κΈ° 전에 각 ν…ŒμŠ€νŠΈκ°€ ν•œλ²ˆ 이상 μ‹€νŒ¨ν•˜λ„λ‘ ν•˜μ‹­μ‹œμ˜€. ν…ŒμŠ€νŠΈλ₯Ό λ§Œμ‘±μ‹œν‚€λŠ” κ°„λ‹¨ν•œ μ½”λ“œλ₯Ό μž‘μ„±ν•˜μ—¬ λΉ λ₯΄κ²Œ λͺ¨λ“ˆμ„ μ‹œμž‘ν•˜μ‹­μ‹œμ˜€ - μ μ‹ μ μœΌλ‘œ λ¦¬νŽ™ν† λ§ν•˜μ—¬ ν”„λ‘œλ•μ…˜ λ“±κΈ‰μ˜ μˆ˜μ€€μœΌλ‘œ κ°€μ Έκ°€μ‹­μ‹œμ˜€. ν™˜κ²½(경둜, OS λ“±)에 λŒ€ν•œ 쒅속성을 ν”Όν•˜μ‹­μ‹œμ˜€.

❌ 그렇지 μ•ŠμœΌλ©΄: μˆ˜μ‹­ λ…„ λ™μ•ˆ μˆ˜μ§‘ 된 μ•„μ£Ό μ†Œμ€‘ν•œ 쑰언을 λ†“μΉ˜κ²Œ 될 κ²ƒμž…λ‹ˆλ‹€.

μ„Ήμ…˜ 2️⃣: λ°±μ—”λ“œ ν…ŒμŠ€νŠΈ

βšͺ ️ 2.1 λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈ 포트폴리였λ₯Ό ν’λΆ€ν•˜κ²Œ ν•˜μ‹­μ‹œμ˜€: λ‹¨μœ„ ν…ŒμŠ€νŠΈμ™€ ν”ΌλΌλ―Έλ“œλ₯Ό λ„˜μ–΄μ„œμ„Έμš”.

βœ… μ΄λ ‡κ²Œ 해라: 10년이 λ„˜μ€ λͺ¨λΈμΈ ν…ŒμŠ€νŠΈ ν”ΌλΌλ―Έλ“œλŠ” μ„Έ 가지 ν…ŒμŠ€νŠΈ μœ ν˜•μ„ μ œμ‹œν•˜κ³  λŒ€λ‹€μˆ˜ 개발자의 ν…ŒμŠ€νŠΈ μ „λž΅μ— 영ν–₯을 μ£ΌλŠ” ν›Œλ₯­ν•œ λͺ¨λΈμž…λ‹ˆλ‹€. λ™μ‹œμ—, λͺ‡ 가지 λ°˜μ§μ΄λŠ” μƒˆλ‘œμš΄ ν…ŒμŠ€νŠΈ κΈ°μˆ λ“€μ΄ λ“±μž₯ν•˜μ˜€μ§€λ§Œ λͺ¨λ‘ ν…ŒμŠ€νŠΈ ν”ΌλΌλ―Έλ“œμ˜ 그림자 λ’€λ‘œ μ‚¬λΌμ‘ŒμŠ΅λ‹ˆλ‹€. μš°λ¦¬κ°€ 졜근 10λ…„κ°„ 보아 온 극적인 기술의 λ³€ν™”λ“€(Microservices, cloud, serverless)을 κ³ λ €ν•  λ•Œ, μ•„μ£Ό 였래된 λͺ¨λΈ ν•˜λ‚˜κ°€ λͺ¨λ“  μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μœ ν˜•μ— μ ν•©ν•˜λ‹€λŠ” 것이 κ°€λŠ₯ν•œκ°€μš”? ν…ŒμŠ€νŠΈ μ„Έκ³„λŠ” μƒˆλ‘œμš΄ κΈ°μˆ μ„ λ°›μ•„λ“€μ΄λŠ” 것을 κ³ λ €ν•˜μ§€ μ•Šλ‚˜μš”?

μ˜€ν•΄λŠ” ν•˜μ§€ λ§ˆμ„Έμš”. 2019 ν…ŒμŠ€νŠΈ ν”ΌλΌλ―Έλ“œμ—μ„œ TDD와 λ‹¨μœ„ ν…ŒμŠ€νŠΈλŠ” μ—¬μ „νžˆ κ°•λ ₯ν•œ 기술이고 μ•„λ§ˆλ„ λ§Žμ€ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ— κ°€μž₯ μ–΄μšΈλ¦¬λŠ” κΈ°μˆ μž…λ‹ˆλ‹€. λ‹€λ₯Έ λͺ¨λΈκ³Ό λ§ˆμ°¬κ°€μ§€λ‘œ, ν…ŒμŠ€νŠΈ λ―ΈλΌλ―Έλ“œλŠ” μœ μš©ν•˜μ§€λ§Œ 그것이 항상 λ§žλŠ” 것은 μ•„λ‹™λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ–΄λ–€ IOT μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ 생각해 λ΄…μ‹œλ‹€. 이 μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ€ λ‹€μˆ˜μ˜ 이벀트λ₯Ό Kafka/RabbitMQ 같은 메세지 λ²„μŠ€λ‘œ 보내고 λ‹€μ‹œ 데이터 μ›¨μ–΄ν•˜μš°μŠ€λ‘œ ν˜λ €λ³΄λƒ…λ‹ˆλ‹€. 그리고 이 데이터듀은 μ–΄λ–€ 뢄석 UIμ—μ„œ μ‘°νšŒλ©λ‹ˆλ‹€. μš°λ¦¬λŠ” 정말 우리의 ν…ŒμŠ€νŠΈ μ˜ˆμ‚°μ˜ 50%λ₯Ό 톡합 쀑심적(intergration-centric)이고 둜직이 거의 μ—†λŠ” μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λŠ”λ° ν• μ• ν•΄μ•Ό ν• κΉŒμš”? μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μœ ν˜•λ“€μ΄ λ‹€μ–‘ν•΄μ§ˆ 수둝(bots, crypto, Alexa-skills) ν…ŒμŠ€νŠΈ ν”ΌλΌλ―Έλ“œκ°€ μ ν•©ν•˜μ§€ μ•Šμ€ μ‹œλ‚˜λ¦¬μ˜€λ“€μ„ λ°œκ²¬ν•  κ°€λŠ₯성이 μ»€μ§‘λ‹ˆλ‹€.

μ§€κΈˆμ΄ λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈ 포트폴리였λ₯Ό λ„“νžˆκ³  더 λ§Žμ€ ν…ŒμŠ€νŠΈ μœ ν˜•λ“€μ— μ΅μˆ™ν•΄μ§ˆ μ‹œκ°„μž…λ‹ˆλ‹€. (λ‹€μŒ ν•­λͺ©μ—μ„œ λͺ‡ 가지 아이디어듀을 μ œμ•ˆν•©λ‹ˆλ‹€.) ν…ŒμŠ€νŠΈ ν”ΌλΌλ―Έλ“œ 같은 λͺ¨λΈλ“€λ„ 염두에 λ‘˜ 뿐만 μ•„λ‹ˆλΌ 당신이 μ§λ©΄ν•˜κ³  μžˆλŠ” ν˜„μ‹€ μ„Έκ³„μ˜ λ¬Έμ œλ“€μ— μ ν•©ν•œ ν…ŒμŠ€νŠΈ μœ ν˜•λ“€μ„ μ°ΎμœΌμ„Έμš”. ("우리 API κΉ¨μ‘Œμ–΄. Consumer-driven contract ν…ŒμŠ€νŠΈ μž‘μ„±ν•˜μž!" μ²˜λŸΌμš”.) μœ„ν—˜μ„± 뢄석을 기반으둜 포λ₯΄ν΄λ¦¬μ˜€λ₯Ό κ΅¬μΆ•ν•˜λŠ” 투자자처럼 λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈλ₯Ό λ‹€μ–‘ν™”ν•˜μ„Έμš” - λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆλŠ” 뢀뢄을 κ°€λŠ ν•˜κ³  잠재적 μœ„ν—˜μ„±μ„ 쀄일 수 μžˆλŠ” 예방 방법을 μ°ΎμœΌμ„Έμš”.

주의 사항 : μ†Œν”„νŠΈμ›¨μ–΄ μ„Έκ³„μ—μ„œμ˜ TDD λ…ΌμŸμ€ μ „ν˜•μ μΈ 잘λͺ»λœ μ΄λΆ„λ²•μž…λ‹ˆλ‹€. μ–΄λ–€ μ‚¬λžŒλ“€μ€ TDDλ₯Ό λͺ¨λ“  곳에 μ μš©ν•˜λΌκ³  μ£Όμž₯ν•˜μ§€λ§Œ, λ‹€λ₯Έ μΌλΆ€λŠ” TDDλ₯Ό μ•…λ§ˆλΌκ³  μƒκ°ν•©λ‹ˆλ‹€. μ ˆλŒ€μ μœΌλ‘œ ν•œμͺ½λ§Œ μ£Όμž₯ν•˜λŠ” μ‚¬λžŒλ“€μ€ λͺ¨λ‘ ν‹€λ ΈμŠ΅λ‹ˆλ‹€ :]

❌ 그렇지 μ•ŠμœΌλ©΄: 당신은 ꡉμž₯ν•œ ROIλ₯Ό μ£ΌλŠ” λͺ‡ 가지 νˆ΄λ“€μ„ 놓칠 κ²ƒμž…λ‹ˆλ‹€. Fuzz, lint, mutation ν…ŒμŠ€νŠΈλ“€μ€ 단 10λΆ„λ§Œμ— λ‹Ήμ‹ μ—κ²Œ κ°€μΉ˜λ₯Ό μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: Cindy Sridharan은 κ·Έλ…€μ˜ ν›Œλ₯­ν•œ κΈ€ β€˜Testing Microservicesβ€Šβ€”β€Šthe sane wayβ€™μ—μ„œ ν’λΆ€ν•œ ν…ŒμŠ€νŠΈ 포트폴리였λ₯Ό μ œμ•ˆν•©λ‹ˆλ‹€. alt text

예제: YouTube: β€œBeyond Unit Tests: 5 Shiny Node.JS Test Types (2018)” (Yoni Goldberg)

alt text

βšͺ ️2.2 μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈκ°€ μ΅œμ„ μ˜ 방법일 수 μžˆλ‹€.

βœ… μ΄λ ‡κ²Œ 해라: 각각의 λ‹¨μœ„ ν…ŒμŠ€νŠΈλŠ” μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 맀우 μž‘μ€ λΆ€λΆ„λ§Œμ„ μ»€λ²„ν•˜κ³  전체λ₯Ό λͺ¨λ‘ μ»€λ²„ν•˜κΈ°μ—λŠ” λΉ„μš©μ΄ 많이 λ“­λ‹ˆλ‹€. λ°˜λ©΄μ—, end-to-end ν…ŒμŠ€νŠΈλŠ” κ°„λ‹¨ν•˜κ²Œ λ§Žμ€ 뢀뢄을 컀버할 수 μžˆμ§€λ§Œ κΉŠμ΄κ°€ μ–•κ³  더 λŠλ¦½λ‹ˆλ‹€. κ·Έλ ‡λ‹€λ©΄ κ· ν˜• 작힌 접근법을 μ μš©ν•˜μ—¬ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ³΄λ‹€λŠ” ν¬μ§€λ§Œ end-to-end ν…ŒμŠ€νŠΈλ³΄λ‹€λŠ” μž‘μ€ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λŠ” �것은 μ–΄λ–¨κΉŒμš”? μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈλŠ” ν…ŒμŠ€νŠΈ μ„Έκ³„μ—μ„œ 잘 μ•Œλ €μ§€μ§€ μ•Šμ€ λ°©λ²•μž…λ‹ˆλ‹€. - μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈλŠ” λ‹€μŒμ˜ 두 가지 이점을 λͺ¨λ‘ μ œκ³΅ν•©λ‹ˆλ‹€: 합리적인 μ„±λŠ₯κ³Ό TDD νŒ¨ν„΄μ„ μ μš©ν•  수 μžˆλŠ” κ°€λŠ₯μ„± + ν˜„μ‹€μ μ΄λ©΄μ„œ ν›Œλ₯­ν•œ 컀버리지

μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈλŠ” 마이크둜 μ„œλΉ„μŠ€ 'λ‹¨μœ„'에 쀑점을 두고 API에 λŒ€ν•˜μ—¬ λ™μž‘ν•©λ‹ˆλ‹€. λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ κ·Έ μžμ²΄μ— μ†ν•œ 것듀 (예λ₯Όλ“€λ©΄, μ‹€μ œ DB λ˜λŠ” ν•΄λ‹Ή DB의 인-λ©”λͺ¨λ¦¬ 버전)은 λͺ¨ν‚Ή(Mock)ν•˜μ§€ μ•Šκ³ , λ‹€λ₯Έ λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ 호좜과 같은 외뢀적인 것은 μŠ€ν…(Stub)ν•©λ‹ˆλ‹€. κ·Έλ ‡κ²Œ ν•¨μœΌλ‘œμ¨ μš°λ¦¬λŠ” μš°λ¦¬κ°€ λ°°ν¬ν•˜λŠ” 것을 ν…ŒμŠ€νŠΈν•˜κ³  μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ λ°”κΉ₯μͺ½μ—μ„œ μ•ˆμͺ½μœΌλ‘œ μ ‘κ·Όν•˜λ©°, μ λ‹Ήν•œ μ‹œκ°„ μ•ˆμ—μ„œ 큰 μžμ‹ κ°μ„ 얻을 수 μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: μ‹œμŠ€ν…œ 컀버리지가 20%에 λΆˆκ³Όν•˜λ‹€λŠ” 것을 κΉ¨λ‹«κΈ°κΉŒμ§€ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜λŠ” 데 였랜 μ‹œκ°„μ΄ 걸릴 수 μžˆμŠ΅λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: Supertestλ₯Ό 톡해 ν”„λ‘œμ„ΈμŠ€ λ‚΄ Express API에 μ ‘κ·Όν•  수 μžˆμŠ΅λ‹ˆλ‹€. (λΉ λ₯΄κ³  λ‹€μ–‘ν•œ 계측을 컀버함)

alt text

βšͺ ️2.3 μ‹ κ·œ λ¦΄λ¦¬μ¦ˆκ°€ API μ‚¬μš©μ„ κΉ¨μ§€κ²Œ ν•˜μ§€ λ§ˆμ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: λ‹Ήμ‹ μ˜ λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€λŠ” λ‹€μˆ˜μ˜ ν΄λΌμ΄μ–ΈνŠΈλ₯Ό 가지고 있고 ν˜Έν™˜μ„±μ˜ 이유둜 μ—¬λŸ¬ λ²„μ „μ˜ μ„œλΉ„μŠ€λ₯Ό μš΄μ˜ν•©λ‹ˆλ‹€ (λͺ¨λ“  μ‚¬λžŒμ„ λ§Œμ‘±μ‹œν‚€κΈ° μœ„ν•΄μ„œ). 그런 μƒν™©μ—μ„œ 당신이 일뢀 ν•„λ“œλ₯Ό λ³€κ²½ν•˜λ©΄ 이 ν•„λ“œλ₯Ό λ―Ώκ³  μ‚¬μš©ν•˜λ˜ 일뢀 μ€‘μš”ν•œ ν΄λΌμ΄μ–ΈνŠΈλŠ” ν™”κ°€ λ‚  κ²ƒμž…λ‹ˆλ‹€. 이것은 톡합(integration) μ„Έκ³„μ—μ„œ ν•΄κ²°ν•˜κΈ° μ–΄λ €μš΄ μ§„ν‡΄μ–‘λ‚œμ— 놓인 λ¬Έμ œμž…λ‹ˆλ‹€: μ„œλ²„ μ‚¬μ΄λ“œκ°€ μ—¬λŸ¬ ν΄λΌμ΄μ–ΈνŠΈλ“€μ˜ λͺ¨λ“  κΈ°λŒ“κ°’μ„ κ³ λ €ν•˜λŠ” 것은 맀우 μ–΄λ €μš΄ μΌμž…λ‹ˆλ‹€. - λ°˜λ©΄μ—, μ„œλ²„κ°€ 릴리즈 λ‚ μ§œλ₯Ό κ²°μ •ν•˜κΈ° λ•Œλ¬Έμ— ν΄λΌμ΄μ–ΈνŠΈλŠ” μ–΄λ– ν•œ ν…ŒμŠ€νŠΈλ„ μˆ˜ν–‰ν•  수 μ—†μŠ΅λ‹ˆλ‹€. μ†ŒλΉ„μž 주도 계약 ν…ŒμŠ€νŠΈ(Consumer-driven contracts)와 PACT ν”„λ ˆμž„μ›Œν¬λŠ” 맀우 파괴적인 λ°©λ²•μœΌλ‘œ μ΄λŸ¬ν•œ ν”„λ‘œμ„ΈμŠ€λ₯Ό ν‘œμ€€ν™”ν•˜κΈ° μœ„ν•΄ λ‚˜νƒ€λ‚¬μŠ΅λ‹ˆλ‹€. - μ„œλ²„κ°€ μ„œλ²„μ˜ ν…ŒμŠ€νŠΈ κ³„νšμ„ κ²°μ •ν•˜μ§€ μ•Šκ³ , ν΄λΌμ΄μ–ΈνŠΈκ°€ μ„œλ²„μ˜ ν…ŒμŠ€νŠΈλ₯Ό κ²°μ •ν•©λ‹ˆλ‹€! PACTλŠ” ν΄λΌμ΄μ–ΈνŠΈμ˜ κΈ°λŒ“κ°’μ„ κΈ°λ‘ν•˜μ—¬ "브둜컀"λΌλŠ” 곡유된 μœ„μΉ˜μ— μ˜¬λ €λ‘˜ 수 μžˆμŠ΅λ‹ˆλ‹€. 그러면 μ„œλ²„λŠ” κ·Έ κΈ°λŒ“κ°’μ„ 당겨 받을 수 있고 λΉŒλ“œν•  λ•Œλ§ˆλ‹€ PACT 라이브러리λ₯Ό μ‚¬μš©ν•˜μ—¬ 깨진 계약(contract - μΆ©μ‘±λ˜μ§€ μ•Šμ€ ν΄λΌμ΄μ–ΈνŠΈμ˜ κΈ°λŒ“κ°’)을 감지할 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λ ‡κ²Œ ν•¨μœΌλ‘œμ¨, λͺ¨λ“  μ„œλ²„-ν΄λΌμ΄μ–ΈνŠΈ API κ°„ μΌμΉ˜ν•˜μ§€ μ•Šμ€ 것듀을 λΉŒλ“œ/CI ν™˜κ²½μ—μ„œ 쑰기에 μž‘μ„ 수 있고 λ‹Ήμ‹ μ˜ 큰 μ ˆλ§κ°μ„ 쀄여쀄 수 μžˆμ„ κ²ƒμž…λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: λŒ€μ•ˆμ€ μˆ˜λ™ λ°°ν¬λ‚˜ 배포에 λŒ€ν•œ 두렀움을 μ•ˆκ³  κ°€λŠ” 것 λΏμž…λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예:

alt text

βšͺ ️ 2.4 λ‹Ήμ‹ μ˜ 미듀웨어λ₯Ό λ…λ¦½μ μœΌλ‘œ ν…ŒμŠ€νŠΈ ν•˜μ‹­μ‹œμ˜€.

βœ… Do: λ§Žμ€ μ‚¬λžŒλ“€μ€ 미듀웨어(Middleware) ν…ŒμŠ€νŠΈλ₯Ό ν”Όν•©λ‹ˆλ‹€. μ™œλƒν•˜λ©΄ 미듀웨어 ν…ŒμŠ€νŠΈλŠ” μ‹œμŠ€ν…œμ˜ μž‘μ€ 뢀뢄일 뿐이고 라이브 Express μ„œλ²„κ°€ ν•„μš”ν•˜κΈ° λ•Œλ¬Έμž…λ‹ˆλ‹€. ν•˜μ§€λ§Œ 두 가지 이유 λͺ¨λ‘ ν‹€λ ΈμŠ΅λ‹ˆλ‹€. - λ―Έλ“€μ›¨μ–΄λŠ” μž‘μ§€λ§Œ λͺ¨λ“  μš”μ²­ λ˜λŠ” λŒ€λΆ€λΆ„μ˜ μš”μ²­μ— 영ν–₯을 미치고, {req,res} JS 객체λ₯Ό κ°€μ§€λŠ” μˆœμˆ˜ν•œ ν•¨μˆ˜λ‘œ μ‰½κ²Œ ν…ŒμŠ€νŠΈν•  수 있기 λ•Œλ¬Έμž…λ‹ˆλ‹€. 미듀웨어 ν•¨μˆ˜λ₯Ό ν…ŒμŠ€νŠΈν•˜κΈ° μœ„ν•΄μ„œλŠ” 단지 ν•¨μˆ˜λ₯Ό 뢈러였고 ν•¨μˆ˜κ°€ μ˜¬λ°”λ₯΄κ²Œ λ™μž‘ν•˜λŠ” 것을 ν™•μΈν•˜κΈ° μœ„ν•΄ {req, res} 객체에 λŒ€ν•œ μΈν„°λ ‰μ…˜μ„ 슀파이(spy)(예λ₯Όλ“€μ–΄ Sinon을 μ‚¬μš©)ν•˜λ©΄ λ©λ‹ˆλ‹€. 라이브러리 node-mock-httpλŠ” 더 λ‚˜μ•„κ°€μ„œ ν–‰μœ„μ— λŒ€ν•œ μŠ€νŒŒμ΄μ™€ ν•¨κ»˜ {req, res} 객체도 ν…ŒμŠ€νŠΈν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, response 객체의 http μƒνƒœκ°€ κΈ°λŒ€ν–ˆλ˜ κ°’κ³Ό μΌμΉ˜ν•˜λŠ”μ§€ μ—¬λΆ€λ₯Ό 확인(assert)ν•  수 μžˆμŠ΅λ‹ˆλ‹€. (μ•„λž˜ 예제λ₯Ό λ³΄μ„Έμš”)

❌ Otherwise: Express λ―Έλ“€μ›¨μ–΄μ—μ„œμ˜ 버그 === λͺ¨λ“  μš”μ²­ λ˜λŠ” λŒ€λΆ€λΆ„μ˜ μš”μ²­μ—μ„œμ˜ 버그

✏ μ½”λ“œ 예제

πŸ‘μ˜¬λ°”λ₯Έ 예: λ„€νŠΈμ›Œν¬ 호좜 없이 전체 Express μ‹œμŠ€ν…œλ„ κΉ¨μš°μ§€ μ•ŠμœΌλ©΄μ„œ 미듀웨어λ₯Ό λ…λ¦½μ μœΌλ‘œ ν…ŒμŠ€νŠΈ

//ν…ŒμŠ€νŠΈν•˜κ³  싢은 미듀웨어
const unitUnderTest = require('./middleware')
const httpMocks = require('node-mocks-http');
//Jest λ¬Έλ²•μœΌλ‘œ Mocha의 describe() & it()κ³Ό 동일
test('헀더에 인증정보가 μ—†λŠ” μš”μ²­μ€, http status 403을 λ¦¬ν„΄ν•΄μ•Όν•œλ‹€.', () => {
  const request = httpMocks.createRequest({
    method: 'GET',
    url: '/user/42',
    headers: {
      authentication: ''
  const response = httpMocks.createResponse();
  unitUnderTest(request, response);

βšͺ ️2.5 정적 뢄석 도ꡬλ₯Ό μ‚¬μš©ν•˜μ—¬ μΈ‘μ •ν•˜κ³  λ¦¬νŒ©ν† λ§ ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: 정적 뢄석 도ꡬλ₯Ό μ‚¬μš©ν•˜λ©΄ μ½”λ“œ ν’ˆμ§ˆμ„ κ°œμ„ ν•˜κ³  μ½”λ“œλ₯Ό μœ μ§€ 관리할 수 μžˆλŠ” 객관적인 방법을 μ œκ³΅ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 정적 뢄석 도ꡬλ₯Ό λ‹Ήμ‹ μ˜ CI λΉŒλ“œμ— μΆ”κ°€ν•˜μ—¬ μ½”λ“œ λƒ„μƒˆ(code smell)κ°€ 발견되면 μ€‘λ‹¨λ˜λ„λ‘ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 정적 뢄석 도ꡬ가 일반적인 린트(lint) 도ꡬ보닀 더 쒋은 점은 μ—¬λŸ¬ νŒŒμΌλ“€μ˜ μ»¨ν…μŠ€νŠΈ μ•ˆμ—μ„œ ν’ˆμ§ˆμ„ κ²€μ‚¬ν•˜κ³ (예: 쀑볡 탐지), κ³ κΈ‰ 뢄석(예: μ½”λ“œ λ³΅μž‘μ„±)을 ν•  수 있으며 μ½”λ“œ μ΄μŠˆμ— λŒ€ν•œ νžˆμŠ€ν† λ¦¬μ™€ ν”„λ‘œμ„ΈμŠ€λ₯Ό 좔적할 수 μžˆλ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. μ‚¬μš©ν•  수 μžˆλŠ” 정적 뢄석 도ꡬ 두 κ°€μ§€λŠ” Sonarqube (2,600+ stars)와 Code Climate (1,500+ stars)μž…λ‹ˆλ‹€.

Credit:: Keith Holliday

❌ 그렇지 μ•ŠμœΌλ©΄: μ½”λ“œ ν’ˆμ§ˆμ΄ 쒋지 μ•ŠμœΌλ©΄ 버그와 μ„±λŠ₯은 λΉ›λ‚˜λŠ” μƒˆ λΌμ΄λΈŒλŸ¬λ¦¬λ‚˜ μ΅œμ‹  κΈ°λŠ₯으둜 ν•΄κ²°ν•  수 μ—†λŠ” λ¬Έμ œκ°€ 될 κ²ƒμž…λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: λ³΅μž‘λ„κ°€ 높은 ν•¨μˆ˜λ₯Ό μ°Ύμ•„λ‚΄λŠ” μƒμš© 도ꡬ인 CodeClimate:

alt text

βšͺ ️ 2.6 λ…Έλ“œ 혼돈(chaos)λŒ€ν•œ μ€€λΉ„μƒνƒœλ₯Ό ν™•μΈν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: μ΄μƒν•˜κ²Œλ„ λŒ€λΆ€λΆ„μ˜ μ†Œν”„νŠΈμ›¨μ–΄ ν…ŒμŠ€νŠΈλŠ” 였직 둜직과 데이터λ₯Ό λŒ€μƒμœΌλ‘œ ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ μ΅œμ•…μ˜ 상황(정말 ν•΄κ²°ν•˜κΈ° 어렡기도 ν•œ 상황) 쀑 μΌλΆ€λŠ” 인프라 μ΄μŠˆμž…λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, ν”„λ‘œμ„ΈμŠ€ λ©”λͺ¨λ¦¬κ°€ κ³ΌλΆ€ν•˜ λ˜κ±°λ‚˜ μ„œλ²„/ν”„λ‘œμ„ΈμŠ€κ°€ μ£½λŠ” 상황, λ˜λŠ” API 속도가 50% μ•„λž˜λ‘œ λ–¨μ–΄μ§ˆ λ•Œ λͺ¨λ‹ˆν„°λ§ μ‹œμŠ€ν…œμ΄ μΈμ‹ν•˜λŠ” 상황에 λŒ€ν•΄μ„œ ν…ŒμŠ€νŠΈν•œ 적이 μžˆλ‚˜μš”? μ΄λŸ¬ν•œ 문제 상황듀을 ν…ŒμŠ€νŠΈν•˜κ³  쀄이기 μœ„ν•΄μ„œ - 카였슀 μ—”μ§€λ‹ˆμ–΄λ§(Chaos engineering)이 λ„·ν”Œλ¦­μŠ€μ— μ˜ν•΄ νƒ„μƒν–ˆμŠ΅λ‹ˆλ‹€. 카였슀 μ—”μ§€λ‹ˆμ–΄λ§μ€ 혼돈(chaos) 상황에 λŒ€ν•œ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ˜ 볡원λ ₯을 ν…ŒμŠ€νŠΈν•˜κΈ° μœ„ν•΄μ„œ 상황에 λŒ€ν•œ 인식, ν”„λ ˆμž„μ›Œν¬, νˆ΄λ“€μ„ μ œκ³΅ν•˜λŠ” 것을 λͺ©ν‘œλ‘œ ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 유λͺ…ν•œ 툴 쀑에 ν•˜λ‚˜μΈ 카였슀 λͺ½ν‚€(chaos monkey)λŠ” μ„œλ²„λ₯Ό λ¬΄μž‘μœ„λ‘œ μ’…λ£Œμ‹œν‚€κ³  μ΄λŸ¬ν•œ 상황에도 μ‚¬μš©μžλŠ” μ„œλΉ„μŠ€λ₯Ό 계속 μ‚¬μš©ν•  수 μžˆμ–΄ μ‹œμŠ€ν…œμ΄ 단일 μ„œλ²„μ— μ˜μ‘΄ν•˜μ§€ μ•Šκ³  μžˆλ‹€λŠ” 것을 ν…ŒμŠ€νŠΈν•©λ‹ˆλ‹€. (μΏ λ²„λ„€ν‹°μŠ€ 버전인 kube-monkeyλŠ” 팟(Pod)을 μ’…λ£Œμ‹œν‚΄) μ΄λŸ¬ν•œ νˆ΄λ“€μ€ λͺ¨λ‘ ν˜ΈμŠ€νŒ…/ν”Œλž«νΌ λ ˆλ²¨μ—μ„œ λ™μž‘ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ 당신이 순수 λ…Έλ“œ ν˜Όλˆμ„ ν…ŒμŠ€νŠΈν•˜κ³  λ°œμƒμ‹œν‚€κ³  μ‹ΆμœΌλ©΄ μ–΄λ–»κ²Œ ν•΄μ•Ό ν• κΉŒμš”? 예λ₯Ό λ“€λ©΄, λ…Έλ“œ ν”„λ‘œμ„ΈμŠ€κ°€ μ–΄λ–»κ²Œ μž‘νžˆμ§€ μ•Šμ€ 였λ₯˜, μ²˜λ¦¬λ˜μ§€ μ•Šμ€ ν”„λ‘œλ―ΈμŠ€ κ±°λΆ€(promise rejection), μ΅œλŒ€λ‘œ ν—ˆμš©λœ 1.7GB에 λŒ€ν•œ v8 λ©”λͺ¨λ¦¬ κ³ΌλΆ€ν•˜λ₯Ό μ²˜λ¦¬ν•˜λŠ”μ§€. ν˜Ήμ€ 이벀트 루프가 자주 차단될 λ•Œ UXκ°€ 만쑱슀럽게 μœ μ§€λ˜λŠ”μ§€ μ—¬λΆ€ 같은 κ²ƒλ“€μ΄μš”. 이 문제λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μ œκ°€ λͺ¨λ“  μ’…λ₯˜μ˜ λ…Έλ“œ κ΄€λ ¨λœ 카였슀 ν–‰μœ„λ₯Ό μ œκ³΅ν•˜λŠ” node-chaos (alpha)λ₯Ό λ§Œλ“€μ—ˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: νƒˆμΆœκ΅¬λŠ” μ—†μŠ΅λ‹ˆλ‹€. λ¨Έν”Όμ˜ 법칙은 μžλΉ„μ—†μ΄ λ‹Ήμ‹ μ˜ μ‹œμŠ€ν…œμ— 타격을 쀄 κ²ƒμž…λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: Node-chaosλŠ” λͺ¨λ“  μ’…λ₯˜μ˜ Node.js ν–‰μœ„λ“€μ„ λ°œμƒμ‹œμΌœμ„œ λ‹Ήμ‹ μ˜ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ μ–Όλ§ˆλ‚˜ 혼돈 μƒνƒœμ— λŒ€ν•œ 볡원λ ₯이 μžˆλŠ”μ§€ ν…ŒμŠ€νŠΈ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

alt text

βšͺ ️2.7 κΈ€λ‘œλ²Œν•œ 초기 ν…ŒμŠ€νŠΈ 데이터 집합을 λ§Œλ“€μ§€ 말고 각 ν…ŒμŠ€νŠΈ λ§ˆλ‹€ 데이터λ₯Ό μΆ”κ°€ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: ν™©κΈˆλ₯ (μ„Ήμ…˜ 0)에 λ”°λ₯΄λ©΄ 각 ν…ŒμŠ€νŠΈλŠ” μ»€ν”Œλ§μ„ λ°©μ§€ν•˜κ³  ν…ŒμŠ€νŠΈ 흐름에 λŒ€ν•΄μ„œ μ‰½κ²Œ μΆ”λ‘ ν•˜κΈ° μœ„ν•΄ μžμ‹ μ˜ DB 데이터듀을 μΆ”κ°€ν•˜κ³  ν•΄λ‹Ή λ°μ΄ν„°λ‘œ ν…ŒμŠ€νŠΈλ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€. ν•˜μ§€λ§Œ ν˜„μ‹€ 세계에선 μ„±λŠ₯ ν–₯상을 μœ„ν•΄ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜κΈ° 전에 초기 데이터λ₯Ό DB에 μΆ”κ°€ν•˜λŠ”(β€˜test fixture’라고 μ•Œλ €μ Έ 있음) ν…ŒμŠ€ν„°λ“€μ— μ˜ν•΄μ„œ 이 κ·œμΉ™μ€ μ’…μ’… 깨지곀 ν•©λ‹ˆλ‹€. μ„±λŠ₯은 μ‹€μ œλ‘œ μ€‘μš”ν•œ λ¬Έμ œμž…λ‹ˆλ‹€. - 이 λ¬Έμ œλŠ” 완화될 수 μžˆμŠ΅λ‹ˆλ‹€ ('μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈ' μ„Ήμ…˜μ„ λ³΄μ„Έμš”). ν•˜μ§€λ§Œ ν…ŒμŠ€νŠΈ λ³΅μž‘μ„±μ€ λŒ€λΆ€λΆ„μ˜ λ‹€λ₯Έ 고렀사항듀을 지배해 λ²„λ¦¬λŠ” λ”μš± κ³ ν†΅μŠ€λŸ° λ¬Έμ œμž…λ‹ˆλ‹€. μ‹€μ§ˆμ μœΌλ‘œ 각 ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€μ— ν•„μš”ν•œ DB λ ˆμ½”λ“œλ§Œ λͺ…μ‹œμ μœΌλ‘œ μΆ”κ°€ν•˜κ³  ν•΄λ‹Ή λ ˆμ½”λ“œλ₯Ό κ°€μ§€κ³ λ§Œ ν…ŒμŠ€νŠΈν•˜μ„Έμš”. λ§Œμ•½ μ„±λŠ₯이 μ€‘μš”ν•œ 문제라면 - 데이터λ₯Ό λ³€κ²½ν•˜μ§€ μ•ŠλŠ” ν…ŒμŠ€νŠΈλ“€μ— λŒ€ν•΄μ„œλ§Œ 초기 데이터λ₯Ό μ±„μš°λŠ” ν˜•νƒœλ‘œ νƒ€ν˜‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€. (예: 쿼리)

❌ 그렇지 μ•ŠμœΌλ©΄: ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•˜κ³  λ°°ν¬λŠ” μ€‘λ‹¨λ˜μ–΄ νŒ€μ›λ“€μ€ μ§€κΈˆ μ†Œμ€‘ν•œ μ‹œκ°„μ„ ν• μ• ν•΄μ•Ό ν•©λ‹ˆλ‹€. 버그가 μžˆμŠ΅λ‹ˆκΉŒ? μ°Ύμ•„λ΄…μ‹œλ‹€, 였 이런 - 두 개의 ν…ŒμŠ€νŠΈκ°€ λ™μΌν•œ ν…ŒμŠ€νŠΈ 데이터(seed data)λ₯Ό λ³€κ²½ν•œ κ²ƒμœΌλ‘œ λ³΄μž…λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: ν…ŒμŠ€νŠΈλŠ” 독립적이지 μ•Šκ³  ν…ŒμŠ€νŠΈλ§ˆλ‹€ κΈ€λ‘œλ²Œ DB 데이터λ₯Ό μ‚¬μš©ν•˜λ„λ‘ 훅이 κ±Έλ €μžˆμŠ΅λ‹ˆλ‹€.

before(() => {
  // DB에 μ‚¬μ΄νŠΈμ™€ μ–΄λ“œλ―Ό 데이터λ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€. λ°μ΄ν„°λŠ” 어디에 μžˆλ‚˜μš”? 외뢀에 μžˆμŠ΅λ‹ˆλ‹€. μ™ΈλΆ€ json νŒŒμΌμ΄λ‚˜ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ ν”„λ ˆμž„μ›Œν¬μ— μžˆμŠ΅λ‹ˆλ‹€. 
  await DB.AddSeedDataFromJson('seed.json');
it("μ‚¬μ΄νŠΈ 이름을 λ³€κ²½ν•˜λ©΄, 성곡 결과값을 λ°›μ•„μ˜¨λ‹€", async () => {
  //"portal"μ΄λΌλŠ” μ΄λ¦„μ˜ μ‚¬μ΄νŠΈκ°€ μžˆλ‹€λŠ” 것을 μ•Œκ³  μžˆμŠ΅λ‹ˆλ‹€. - μ”¨λ“œ νŒŒμΌμ—μ„œ λ΄€μŠ΅λ‹ˆλ‹€. 
  const siteToUpdate = await SiteService.getSiteByName("Portal");
  const updateNameResult = await SiteService.changeName(siteToUpdate, "newName");
it("μ‚¬μ΄νŠΈ μ΄λ¦„μœΌλ‘œ μ‘°νšŒν–ˆμ„λ•Œ, ν•΄λ‹Ή μ‚¬μ΄νŠΈλ₯Ό κ°€μ Έμ˜¨λ‹€", async () => {
  //"portal"μ΄λΌλŠ” μ΄λ¦„μ˜ μ‚¬μ΄νŠΈκ°€ μžˆλ‹€λŠ” 것을 μ•Œκ³  μžˆμŠ΅λ‹ˆλ‹€. - μ”¨λ“œ νŒŒμΌμ—μ„œ λ΄€μŠ΅λ‹ˆλ‹€. 
  const siteToCheck = await SiteService.getSiteByName("Portal");
  expect("Portal"); //μ‹€νŒ¨! 이전 ν…ŒμŠ€νŠΈμ—μ„œ 이름이 λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€ :[

πŸ‘ μ˜¬λ°”λ₯Έ 예: ν…ŒμŠ€νŠΈ μ•ˆμ—μ„œλ§Œ λ¨Έλ¬Όλ©° 각 ν…ŒμŠ€νŠΈλŠ” μžμ‹ μ˜ 데이터 μ„ΈνŠΈ μ•ˆμ—μ„œλ§Œ λ™μž‘ν•©λ‹ˆλ‹€.

it("μ‚¬μ΄νŠΈ 이름을 λ³€κ²½ν•˜λ©΄, 성곡 결과값을 λ°›μ•„μ˜¨λ‹€", async () => {
  //ν…ŒμŠ€νŠΈλŠ” μƒˆλ‘œμš΄ μ‹ κ·œ λ ˆμ½”λ“œλ₯Ό μΆ”κ°€ν•˜κ³  κ·Έ λ ˆμ½”λ“œλ₯Ό 가지고 λ™μž‘ν•©λ‹ˆλ‹€.
  const siteUnderTest = await SiteService.addSite({
    name: "siteForUpdateTest"
  const updateNameResult = await SiteService.changeName(siteUnderTest, "newName");

μ„Ήμ…˜ 3️⃣: ν”„λ‘ νŠΈμ—”λ“œ ν…ŒμŠ€νŠΈ

βšͺ ️ 3.1. κΈ°λŠ₯μœΌλ‘œλΆ€ν„° 화면을 λΆ„λ¦¬ν•˜μ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: μ»΄ν¬λ„ŒνŠΈ λ‘œμ§μ„ ν…ŒμŠ€νŠΈν• λ•Œ, ν™”λ©΄μ˜ 세뢀사항듀은 μ œμ™Έλ˜μ–΄μ•Όν•  λ…Έμ΄μ¦ˆκ°€ λ©λ‹ˆλ‹€. 그것을 μ œμ™Έν•¨μœΌλ‘œμ¨ λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈλ“€μ€ μˆœμˆ˜ν•œ 데이터에 집쀑할 수 μžˆμŠ΅λ‹ˆλ‹€. μ‹€μ œλ‘œ, κ·Έλž˜ν”½ κ΅¬ν˜„μ— λ„ˆλ¬΄ κ²°ν•©λ˜μ§€ μ•ŠλŠ” 좔상적인 방법을 톡해 μš”κ΅¬λ˜μ–΄μ§€λŠ” 데이터λ₯Ό λ§ˆν¬μ—…μœΌλ‘œλΆ€ν„° μΆ”μΆœν•˜μ‹­μ‹œμ˜€. 그리고 느리게 λ§Œλ“œλŠ” μ• λ‹ˆλ©”μ΄μ…˜λ“€μ„ μ œμ™Έν•œ 였직 μˆœμˆ˜ν•œ 데이터λ₯Ό κ²€μ¦ν•˜μ‹­μ‹œμ˜€(vs HTML/CSS ν™”λ©΄ 세뢀사항). 당신은 λ Œλ”λ§ν•˜λŠ” 것을 ν”Όν•˜κ³  였직 ν™”λ©΄μ˜ λ’·λΆ€λΆ„(μ„œλΉ„μŠ€, μ•‘μ…˜, μŠ€ν† μ–΄λ“±κ³Ό 같은)λ§Œμ„ ν…ŒμŠ€νŠΈ ν•˜λ €κ³  ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. ν•˜μ§€λ§Œ, 이것은 μ‹€μ œμ™€ 같지도 μ•ŠμœΌλ©° 심지어 화면에 μ˜¬λ°”λ₯Έ 데이터가 λ„λ‹¬ν•˜μ§€ μ•Šμ€ 경우λ₯Ό λ‚˜νƒ€λ‚΄μ§€λ„ μ•ŠλŠ” κ°€μ§œ ν…ŒμŠ€νŠΈμ—μ„œμ˜ κ²°κ³Όκ°€ 될 것 μž…λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈμ˜ μˆœμˆ˜ν•˜κ²Œ κ³„μ‚°λœ λ°μ΄ν„°λŠ” 10ms 내에 μ€€λΉ„λ μˆ˜λ„ μžˆμ§€λ§Œ, 전체 ν…ŒμŠ€νŠΈλŠ” ν™”λ €ν•˜κ³  λΆˆν•„μš”ν•œ μ• λ‹ˆλ©”μ΄μ…˜ λ•Œλ¬Έμ— 500ms(100 ν…ŒμŠ€νŠΈ = 1λΆ„) λ™μ•ˆ 지속될 것 μž…λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: ν™”λ©΄μ˜ 세뢀사항을 λΉΌλ‚΄λŠ” 것

test('였직 VIPλ₯Ό λ³΄κΈ°μœ„ν•΄ μ‚¬μš©μžλͺ©λ‘μ„ ν‘œμ‹œν–ˆμ„λ•Œ, 였직 VIP λ©€λ²„λ“€λ§Œ 보여져야 ν•œλ‹€', () => {
  // Arrange
  const allUsers = [
   { id: 1, name: 'Yoni Goldberg', vip: false }, 
   { id: 2, name: 'John Doe', vip: true }

  // Act
  const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>);

  // Assert - μš°μ„  ν™”λ©΄μœΌλ‘œλΆ€ν„° 데이터λ₯Ό μΆ”μΆœ
  const allRenderedUsers = getAllByTestId('user').map(uiElement => uiElement.textContent);
  const allRealVIPUsers = allUsers.filter((user) => =>;
  expect(allRenderedUsers).toEqual(allRealVIPUsers); // 화면에 μ•„λ‹Œ 데이터λ₯Ό 비ꡐ

πŸ‘Ž 잘λͺ»λœ 예: ν™”λ©΄ 세뢀사항듀과 데이터λ₯Ό μ„žμ–΄μ„œ 검증

test('였직 VIPλ₯Ό λ³΄κΈ°μœ„ν•΄ μ‚¬μš©μžλͺ©λ‘μ„ ν‘œμ‹œν–ˆμ„λ•Œ, 였직 VIP λ©€λ²„λ“€λ§Œ 보여져야 ν•œλ‹€', () => {
  // Arrange
  const allUsers = [
   {id: 1, name: 'Yoni Goldberg', vip: false }, 
   {id: 2, name: 'John Doe', vip: true }

  // Act
  const { getAllByTestId } = render(<UsersList users={allUsers} showOnlyVIP={true}/>);

  // Assert - ν™”λ©΄κ³Ό 데이터λ₯Ό μ„žμ–΄μ„œ 검증
  expect(getAllByTestId('user')).toEqual('[<li data-testid="user">John Doe</li>]');

βšͺ ️ 3.2 λ³€ν•˜μ§€ μ•Šμ€ μš”μ†Œλ“€μ— κΈ°λ°˜ν•΄μ„œ HTML μ—˜λ¦¬λ¨ΌνŠΈλ“€μ„ μ°ΎμœΌμ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: CSS κ²€μƒ‰μžλ“€κ³Ό λ‹€λ₯΄κ²Œ 양식 λ ˆμ΄λΈ”λ“€κ³Ό 같이 κ·Έλž˜ν”½ 변경에도 살아남을 μš”μ†Œλ“€μ„ 기반으둜 HTML μ—˜λ¦¬λ¨ΌνŠΈλ“€μ„ μ°ΎμœΌμ‹­μ‹œμ˜€. λ§Œμ•½ μ„€κ³„λœ μ—˜λ¦¬λ¨ΌνŠΈκ°€ 이와 같은 μš”μ†Œλ“€μ„ 가지고 μžˆμ§€ μ•Šλ‹€λ©΄, 'test-id-submit-button' κ³Ό 같이 ν…ŒμŠ€νŠΈμ— ν•œμ •λœ μš”μ†Œλ₯Ό λ§Œλ“œμ‹­μ‹œμ˜€. 이 방법은 λ‹Ήμ‹ μ˜ κΈ°λŠ₯/둜직 ν…ŒμŠ€νŠΈλ“€μ΄ λ£©μ•€ν•„λ•Œλ¬Έμ— μ ˆλŒ€ 망가지지 μ•Šμ„ 것을 보μž₯ν•  뿐만 μ•„λ‹ˆλΌ, 이 μ—˜λ¦¬λ¨ΌνŠΈμ™€ μš”μ†Œκ°€ ν…ŒμŠ€νŠΈμ— μ˜ν•΄ μ‚¬μš©λ˜μ–΄μ§€κ³  μ œκ±°λ˜μ–΄μ„œλŠ” μ•ˆλœλ‹€λŠ”κ²ƒμ„ νŒ€ μ „μ²΄μ—κ²Œ λͺ…ν™•ν•˜κ²Œ ν•©λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: 당신은 둜그인 κΈ°λŠ₯을 ν…ŒμŠ€νŠΈν•˜κΈ°λ₯Ό μ›ν•©λ‹ˆλ‹€. 이 κΈ°λŠ₯은 λ§Žμ€ μ»΄ν¬λ„ŒνŠΈλ“€, 둜직 그리고 μ„œλΉ„μŠ€λ“€μ— 걸쳐져 있고 λͺ¨λ“  것은 μ™„λ²½ν•˜κ²Œ μ€€λΉ„λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€ - μŠ€ν…, 슀파이, Ajax ν˜ΈμΆœμ€ κ²©λ¦¬λ˜μ–΄μ Έ μžˆμŠ΅λ‹ˆλ‹€. λͺ¨λ“ κ²ƒμ€ μ™„λ²½ν•œ 것 처럼 λ³΄μž…λ‹ˆλ‹€. κ·Έλ ‡μ§€λ§Œ, 이 ν…ŒμŠ€νŠΈλŠ” λ””μžμ΄λ„ˆμ— μ˜ν•΄ div 클래슀 이름이 'thick-border' μ—μ„œ 'thin-border'둜 λ°”λ€Œμ—ˆκΈ° λ•Œλ¬Έμ— μ‹€νŒ¨ν•©λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•΄ ν•œμ •λœ μš”μ†Œλ₯Ό μ‚¬μš©ν•΄μ„œ μ—˜λ¦¬λ¨ΌνŠΈλ₯Ό μ°ΎμœΌμ‹­μ‹œμ˜€

// the markup code (part of React component)
  <Badge pill className="fixed_badge" variant="dark">
    <span data-testid="errorsLabel">{value}</span> <!-- note the attribute data-testid -->
// react-testing-libraryλ₯Ό μ‚¬μš©ν•œ 예제
  test('metric에 데이터가 μ „λ‹¬λ˜μ§€ μ•ŠμœΌλ©΄, 0을 κΈ°λ³Έκ°’μœΌλ‘œ 보여쀀닀', () => {
    // Arrange
    const metricValue = undefined;

    // Act
    const { getByTestId } = render(<dashboardMetric value={undefined}/>);    

πŸ‘Ž 잘λͺ»λœ 예: CSS μš”μ†Œλ“€μ— 의쑴

<!-- the markup code (part of React component) -->
<span id="metric" className="d-flex-column">{value}</span> <!-- λ§Œμ•½ λ””μžμ΄λ„ˆκ°€ 클래슀λ₯Ό λ³€κ²½ν•œλ‹€λ©΄? -->
// enzyme을 μ‚¬μš©ν•œ 예제
test('데이터가 μ „λ‹¬λ˜μ§€ μ•ŠμœΌλ©΄, 0을 보여쀀닀', () => {
    // ...

βšͺ ️ 3.3 κ°€λŠ₯ν•œν•œ, μ‹€μ œμ™€ κ°™κ³  μ™„μ „νžˆ λ Œλ”λ§λœ μ»΄ν¬λ„ŒνŠΈλ₯Ό ν…ŒμŠ€νŠΈν•˜μ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: μ λ‹Ήν•œ 크기가 λ•Œλ§ˆλ‹€ μ‚¬μš©μžκ°€ ν•˜λŠ” κ²ƒμ²˜λŸΌ μ™ΈλΆ€λ‘œλΆ€ν„° μ»΄ν¬λ„ŒνŠΈλ₯Ό ν…ŒμŠ€νŠΈν•˜κ³ , ν™”λ©΄λ₯Ό μ™„μ „νžˆ λ Œλ”λ§ν•˜κ³ , 그에 따라 쑰치λ₯Ό μ·¨ν•˜κ³  λ Œλ”λ§ 된 화면이 μ˜ˆμƒλŒ€λ‘œ μž‘λ™ν•˜λŠ”μ§€ ν™•μΈν•˜μ‹­μ‹œμ˜€. λͺ¨λ“  μ’…λ₯˜μ˜ λͺ©ν‚Ή, λΆ€λΆ„ 및 얕은 λ Œλ”λ§μ„ ν”Όν•˜μ‹­μ‹œμ˜€. 이 접근은 μ„ΈλΆ€μ •λ³΄μ˜ λΆ€μ‘±μœΌλ‘œ 인해 걸리지 μ•ŠλŠ” 버그가 λ°œμƒν•  수 있으며, λ‚΄λΆ€μš”μ†Œλ“€κ³Ό ν•¨κ»˜ 지저뢄해진 ν…ŒμŠ€νŠΈλ“€κ³Ό 같이 μœ μ§€λ³΄μˆ˜λ₯Ό ν•˜κΈ° μ–΄λ ΅κ²Œ λ§Œλ“€ μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. (see bullet 'Favour blackbox testing'). λ§Œμ•½ μžμ‹ μ»΄ν¬λ„ŒνŠΈλ“€μ€‘ ν•˜λ‚˜κ°€ μ‹¬κ°ν•˜κ²Œ λŠλ €μ§€κ²Œ ν•˜κ±°λ‚˜(예: μ• λ‹ˆλ©”μ΄μ…˜)) 섀정을 λ³΅μž‘ν•˜κ²Œ ν•˜λŠ” κ²½μš°μ—λŠ”, ν•΄λ‹Ήμš”μ†Œλ₯Ό κ°€μƒμœΌλ‘œ μ²˜λ¦¬ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

μ£Όμ˜ν•  점: 이 κΈ°μˆ μ€ μžμ‹ μ»΄ν¬λ„ŒνŠΈλ“€μ˜ 크기가 μ λ‹Ήν•˜κ²Œ λ¬Άμ—¬μžˆλŠ”, μ†Œν˜• ν˜Ήμ€ μ€‘ν˜• μ»΄ν¬λ„ŒνŠΈλ“€μ—κ²Œ μ ν•©ν•©λ‹ˆλ‹€. λ„ˆλ¬΄ λ§Žμ€ μžμ‹λ“€κ³Ό ν•¨κ»˜ λ Œλ”λ§λœ μ»΄ν¬λ„ŒνŠΈλŠ”, ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•œ 원인(근본원인 뢄석)을 μΆ”λ‘ ν•˜κΈ°λ„ μ–΄λ ΅κ³  맀우 느렀질 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ κ²½μš°λ“€μ—μ„œλŠ”, λΆ€λͺ¨μ— λŒ€ν•΄μ„œλŠ” λͺ‡κ°€μ§€ ν…ŒμŠ€νŠΈλ§Œμ„ μž‘μ„±ν•˜κ³ , μžμ‹λ“€μ— λŒ€ν•΄μ„œ 더 λ§Žμ€ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜μ‹­μ‹œμ˜€.

❌ 그렇지 μ•ŠμœΌλ©΄: λ‚΄λΆ€ λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•˜μ—¬ μ»΄ν¬λ„ŒνŠΈμ˜ 내뢀에 영ν–₯을 μ£Όκ³  그리고 λ‚΄λΆ€μ˜ μƒνƒœλ₯Ό ν™•μΈν•œλ‹€λ©΄ - 당신이 μ»΄ν¬λ„ŒνŠΈμ˜ κ΅¬ν˜„μ„ λ¦¬νŒ©ν† λ§ν• λ•Œ, λͺ¨λ“  ν…ŒμŠ€νŠΈλ„ ν•¨κ»˜ λ³€κ²½ν•΄μ•Ό ν•©λ‹ˆλ‹€. 당신은 μœ μ§€λ³΄μˆ˜λ₯Ό μœ„ν•œ 그런 μ—¬μœ κ°€ μžˆμŠ΅λ‹ˆκΉŒ?

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: μ™„μ „ν•˜κ²Œ λ Œλ”λ§λœ μ»΄ν¬λ„ŒνŠΈμ™€ ν•¨κ»˜ μ‹€μ œμ™€ 같은 λ™μž‘

class Calendar extends React.Component {
  static defaultProps = {showFilters: false}
  render() {
    return (
        A filters panel with a button to hide/show filters
        <FiltersPanel showFilter={showFilters} title='Choose Filters'/>

//Examples use React & Enzyme
test('μ‹€μ œμ μΈ μ ‘κ·Ό: 필터듀을 ν΄λ¦­ν•˜λ©΄, 필터듀이 화면에 ν‘œμ‹œλœλ‹€', () => {
    // Arrange
    const wrapper = mount(<Calendar showFilters={false} />)

    // Act

    // Assert
    expect(wrapper.text().includes('Choose Filter'));
    // μ‚¬μš©μžκ°€ μš”μ†Œμ— μ ‘κ·Όν•˜λŠ” 방법: ν…μŠ€νŠΈλ₯Ό 이용

πŸ‘Ž 잘λͺ»λœ 예: 얕은 λ Œλ”λ§κ³Ό ν•¨κ»˜ μ‹€μ œλ₯Ό λͺ©ν‚Ή

test('얕은/λͺ©ν‚Ή μ ‘κ·Ό: 필터듀을 ν΄λ¦­ν•˜λ©΄, 필터듀이 화면에 ν‘œμ‹œλœλ‹€', () => {
    // Arrange
    const wrapper = shallow(<Calendar showFilters={false} title='Choose Filter'/>)

    // Act
    // λ‚΄λΆ€λ₯Ό νƒ­ν•˜κ³ , 화면을 λ¬΄μ‹œν•œμ±„ λ©”μ†Œλ“œλ₯Ό 호좜. ν™”μ΄νŠΈλ°•μŠ€ μ ‘κ·Ό

    // Assert
    expect(wrapper.find('Filter').props()).toEqual({title: 'Choose Filter'});
    // name을 λ³€κ²½ν•˜κ±°λ‚˜, κ΄€λ ¨λœ λ‹€λ₯Έ 것듀을 μ „λ‹¬ν•˜μ§€ μ•ŠλŠ”λ‹€λ©΄ μ–΄λ–»κ²Œ 될까?

βšͺ ️ 3.4 μŠ¬λ¦½μ„ μ‚¬μš©ν•˜μ§€ λ§ˆμ‹­μ‹œμ˜€. ν”„λ ˆμž„μ›Œν¬μ—μ„œ 비동기 μ΄λ²€νŠΈλ“€μ„ μœ„ν•΄ μ§€μ›ν•˜λŠ” λ‚΄μž₯ κΈ°λŠ₯을 μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. 그리고 속도λ₯Ό 높이렀 λ…Έλ ₯ν•˜μ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: λŒ€λΆ€λΆ„μ˜ κ²½μš°μ— ν…ŒμŠ€νŠΈ μ™„λ£Œμ‹œκ°„μ€ μ•Œ 수 μ—†μŠ΅λ‹ˆλ‹€. (예: μ• λ‹ˆλ©”μ΄μ…˜μ€ μš”μ†Œμ˜ μΆœν˜„μ„ μ§€μ—°μ‹œν‚΄) - 이런 κ²½μš°μ—λŠ” 슬립(예: setTimeout)을 ν”Όν•˜κ³ , λŒ€λΆ€λΆ„μ˜ ν”Œλž«νΌλ“€μ΄ μ œκ³΅ν•˜λŠ” 더 결정적인 λ©”μ†Œλ“œλ“€μ„ μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. λͺ‡λͺ‡ λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ€ awaiting κΈ°λŠ₯을 ν—ˆμš©ν•©λ‹ˆλ‹€. (예: Cypress cy.request('url')), λŒ€κΈ°λ₯Ό μœ„ν•œ λ‹€λ₯Έ API @testing-library/dom method wait(expect(element)). λ•Œλ•Œλ‘œ, 더 μš°μ•„ν•œ 방법은 API같이 느린 μžμ›μ„ μŠ€ν…ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 그런 ν›„ μ‘λ‹΅μˆœκ°„μ΄ 결정적이 되면, μ»΄ν¬λ„ŒνŠΈλ₯Ό λͺ…μ‹œμ μœΌλ‘œ λ‹€μ‹œ λ Œλ”λ§ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ™ΈλΆ€ μ»΄ν¬λ„ŒνŠΈκ°€ μŠ¬λ¦½μƒνƒœμΌλ•ŒλŠ”, hurry-up the clockκ°€ μœ μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μŠ¬λ¦½μ€ λ‹Ήμ‹ μ˜ ν…ŒμŠ€νŠΈλ₯Ό 느리고 μœ„ν—˜ν•˜κ²Œ λ§Œλ“€κΈ° λ•Œλ¬Έμ— ν”Όν•΄μ•Όν•  νŒ¨ν„΄μž…λ‹ˆλ‹€(λ„ˆλ¬΄ 짧은 μ‹œκ°„ κΈ°λ‹€λ €μ•Όν•  경우). λ§Œμ•½ 슬립과 폴링이 필연적이고 ν…ŒμŠ€νŠΈ ν”„λ ˆμž„μ›Œν¬μ˜ 지원이 μ—†λ‹€λ©΄, wait-for-expect와 같은 λΌμ΄λΈŒλŸ¬λ¦¬λ“€μ΄ μ€€κ²°μ • μ†”λ£¨μ…˜μœΌλ‘œμ„œ 도움을 쀄 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: 였랜 μ‹œκ°„λ™μ•ˆ μŠ¬λ¦½ν•˜λŠ” 경우, ν…ŒμŠ€νŠΈλŠ” 더 느렀질 것 μž…λ‹ˆλ‹€. μŠ¬λ¦½ν• λ•Œ, ν…ŒμŠ€νŠΈμ€‘μΈ μœ λ‹›μ΄ 제 μ‹œκ°„μ— λ°˜μ‘ν•˜μ§€ μ•ŠμœΌλ©΄ ν…ŒμŠ€νŠΈλŠ” μ‹€νŒ¨ν•  것 μž…λ‹ˆλ‹€. κ·Έλž˜μ„œ 그것은 ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•˜λŠ” 약점과 λ‚˜μœ μ„±λŠ₯κ°„μ˜ νŠΈλ ˆμ΄λ“œ μ˜€ν”„λ₯Ό κ°€μ§€κ²Œ λ©λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: 비동기 싀행이 μ™„λ£Œλ λ•Œ μ²˜λ¦¬λ˜λŠ” E2E API (Cypress)

// using Cypress
cy.get('#show-products').click()// navigate
cy.wait('@products')// wait for route to appear
// λΌμš°νŠΈκ°€ μ€€λΉ„λ˜λ©΄ μ‹€ν–‰ λ©λ‹ˆλ‹€.

πŸ‘ μ˜¬λ°”λ₯Έ 예: DOM μš”μ†Œλ₯Ό κΈ°λ‹€λ¦¬λŠ” ν…ŒμŠ€νŠΈ 라이브러리

// @testing-library/dom
test('movie title appears', async () => {
    // μš”μ†ŒλŠ” μ΄ˆκΈ°μ— 쑴재 ν•˜μ§€ μ•ŠμŒ...

    // μΆœν˜„μ„ λŒ€κΈ°
    await wait(() => {
        expect(getByText('the lion king')).toBeInTheDocument()

    // μΆœν˜„μ„ κΈ°λ‹€λ¦° ν›„ μš”μ†Œλ₯Ό 리턴
    const movie = await waitForElement(() => getByText('the lion king'))

πŸ‘Ž 잘λͺ»λœ 예: μ‚¬μš©μž μ •μ˜ 슬립 μ½”λ“œ

test('movie title appears', async () => {
    // μ΄ˆκΈ°μ— μš”μ†Œκ°€ 쑴재 ν•˜μ§€ μ•ŠμŒ...

    // μ‚¬μš©μž μ •μ˜ λŒ€κΈ° 둜직 (주의: 맀우 λ‹¨μˆœ, νƒ€μž„μ•„μ›ƒμ΄ μ•„λ‹˜)
    const interval = setInterval(() => {
        const found = getByText('the lion king');
            expect(getByText('the lion king')).toBeInTheDocument();
    }, 100);

    // μΆœν˜„μ„ κΈ°λ‹€λ¦° ν›„ μš”μ†Œλ₯Ό 리턴
    const movie = await waitForElement(() => getByText('the lion king'))

βšͺ ️ 3.5. ν™”λ©΄μ˜ λ‚΄μš©μ΄ λ„€νŠΈμ›Œν¬λ₯Ό 톡해 μ–΄λ–»κ²Œ μ œκ³΅λ μ§€ ν™•μΈν•˜μ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: λ„€νŠΈμ›Œν¬λ₯Ό μ κ²€ν•˜κ³  νŽ˜μ΄μ§€λ‘œλ“œ μƒνƒœλ₯Ό 확인 ν•  수 μžˆλŠ” λ„€νŠΈμ›Œν¬ μ•‘ν‹°λΈŒ λͺ¨λ‹ˆν„°λ₯Ό μ μš©ν•˜μ‹­μ‹œμ˜€. pingdom, AWS CloudWatch, gcp StackDriver λ₯Ό μ‚¬μš©ν•˜λ©΄ μ‰½κ²Œ μ„œλ²„μ˜ μƒνƒœμ™€ response λ‚΄μš©μ„ λͺ¨λ‹ˆν„°λ§ ν•˜λ„λ‘ μ„€μ • ν•  수 있고, ν™”λ©΄μ—μ„œμ˜ 였λ₯˜λŠ” μ΅œμ†Œν™” ν•˜λ©΄μ„œ ν…ŒμŠ€νŠΈλ₯Ό 진행 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이듀은 ν”„λ‘ νŠΈμ—”λ“œμ— μ΅œμ ν™” 된 λ‹€λ₯Έ 도ꡬ(lighthouse, pagespeed) 보닀 μ„ ν˜Έλ˜κ³  있고, 더 λ‹€μ–‘ν•œ 뢄석 κΈ°λŠ₯을 제곡 ν•©λ‹ˆλ‹€. ν…ŒμŠ€νŠΈλŠ” νŽ˜μ΄μ§€ λ‘œλ”©μ‹œκ°„μ΄λ‚˜, μ£Όμš” ν•­λͺ©μ˜ λ‘œλ”©, 화면이 μΈν„°λ ‰ν‹°λΈŒ ν•˜κ²Œ λ˜λŠ” μ‹œκ°„κ³Ό 같이 화면에 μ§μ ‘μ μœΌλ‘œ 영ν–₯을 μ£ΌλŠ” ν•­λͺ©μ΄λ‚˜ μƒνƒœμ— 쀑점을 둬야 ν•©λ‹ˆλ‹€. 무엇보닀 contentsκ°€ μ••μΆ• λ˜μ—ˆλŠ”μ§€, 첫 byte μ‘λ‹΅κΉŒμ§€μ˜ μ‹œκ°„, image μ΅œμ ν™”, μ μ ˆν•œ DOM 의 크기 νŒŒμ•…, SSL λ“±μ˜ 기술적 μ΄μŠˆμ— 쀑점을 둬야 ν•©λ‹ˆλ‹€. 이런 ν•­λͺ©μ— λŒ€ν•œ λͺ¨λ‹ˆν„°λ§μ€ κ°œλ°œμ‹œμ μ΄λ‚˜, CI, 24x7 운영 쀑 μˆ˜ν–‰ν•  것을 ꢌμž₯ ν•©λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: μ™„λ²½ν•œ UI λ₯Ό κ°œλ°œν•΄ 두고, 100% κΈ°λŠ₯ ν…ŒμŠ€νŠΈκΉŒμ§€ μ™„λ£Œ ν–ˆμ§€λ§Œ, μ΅œμ•…μ˜ UXλ₯Ό μ œκ³΅ν•˜κ²Œ λ˜κ±°λ‚˜ CDN의 μ„€μ • 였λ₯˜ λ“±μ˜ 이유 λ•Œλ¬Έμ— λ„ˆλ¬΄ 느린 μ„œλΉ„μŠ€λ₯Ό μ œκ³΅ν•˜κ²Œ 될 수 μžˆμŠ΅λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: Lighthouse νŽ˜μ΄μ§€ λ‘œλ”© 검사 λ³΄κ³ μ„œ

βšͺ ️ 3.6 λ°±μ—”λ“œ API와 같이 자주 멈좜 수 μžˆκ±°λ‚˜ 느린 λ¦¬μ†ŒμŠ€λŠ” stub ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: μ£Όμš” κΈ°λŠ₯에 λŒ€ν•œ ν…ŒμŠ€νŠΈ μ½”λ“œ(E2Eν…ŒμŠ€νŠΈ μ•„λ‹˜) μž‘μ„± μ‹œ, backend API 처럼 κ·Έ κΈ°λŠ₯의 μ£Ό μ—­ν• μ—μ„œ λ²—μ–΄λ‚˜λŠ” ν•­λͺ©μ€ μ œμ™Έν•  수 μžˆλ„λ‘ stub(예: test double) ν•˜μ‹­μ‹œμ˜€. μ‹€μ œ λ„€νŠΈμ›Œν¬λ₯Ό ν˜ΈμΆœν•˜μ§€ 말고, test double 라이브러리(Sinon, Test doubles)λ₯Ό μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. 이 경우 κ°€μž₯ 큰 μž₯점은 μ˜ˆμƒμΉ˜ λͺ»ν•œ ν…ŒμŠ€νŠΈ μ‹€νŒ¨λ₯Ό 예방 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ½”λ“œλ‹¨μ—μ„œ API μ •μ˜μ— 따라 testλ₯Ό μ μš©ν•˜λŠ” μž‘μ—…μ€ μ•ˆμ •μ μ΄μ§€ μ•Šκ³  λ‹Ήμ‹ μ˜ componentλŠ” λ¬Έμ œκ°€ μ—†μ§€λ§Œ μˆ˜μ‹œλ‘œ testκ°€ μ‹€νŒ¨ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.(운영 μƒνƒœμ˜ env 섀정은 testingμ—μ„œλŠ” μ‚¬μš©ν•˜μ§€ λ§ˆμ„Έμš”. API μš”μ²­μ— 병λͺ©μ΄ λ°œμƒ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.) μ΄λŸ°μ‹μœΌλ‘œ ν•΄μ„œ API 응닡 데이터가 μ—†λŠ” 경우, μ—λŸ¬κ°€ μ‘λ‹΅λ˜λŠ” 경우 λ“±μ˜ λ‹€μ–‘ν•œ API μƒνƒœμ— λ”°λ₯Έ ν…ŒμŠ€νŠΈλ₯Ό 진행 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œν•œλ²ˆ κ°•μ‘°ν•˜μ§€λ§Œ μ‹€μ œ λ„€νŠΈμ›Œν¬ ν˜ΈμΆœμ€ testλ₯Ό 맀우 느리게 λ§Œλ“€κ²ƒμž…λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: 평균 ν…ŒμŠ€νŠΈ μ‹€ν–‰ μ‹œκ°„μ΄ λͺ‡ ms 인 경우, API 호좜둜 κ°œλ‹Ή μ΅œμ†Œ 100ms μ‹œκ°„μ΄ 걸리게 되고 ν…ŒμŠ€νŠΈλŠ” μ•½ 20λ°° λŠλ €μ§€κ²Œ λ©λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: Stubbing ν•˜κ±°λ‚˜ API 응닡값 λ³€μ‘°

// unit under test
export default function ProductsList() { 
    const [products, setProducts] = useState(false)

    const fetchProducts = async() => {
      const products = await axios.get('api/products')

    useEffect(() => {
    }, []);

  return products ? <div>{products}</div> : <div data-testid='no-products-message'>No products</div>

// test
test('productsκ°€ μ—†λŠ” 경우, μ μ ˆν•œ λ©”μ‹œμ§€ ν‘œμ‹œν•˜κΈ°', () => {
    // Arrange

    // Act
    const {getByTestId} = render(<ProductsList/>);

    // Assert

βšͺ ️ 3.7 전체 μ‹œμŠ€ν…œμ— 걸친 μ—”λ“œ-투-μ—”λ“œ ν…ŒμŠ€νŠΈκ°€ 거의 μ—†μŠ΅λ‹ˆλ‹€.

βœ… μ΄λ ‡κ²Œ 해라: E2E(end-to-end)λŠ” 일반적으둜 μ‹€μ œ λΈŒλΌμš°μ €λ₯Ό μ‚¬μš©ν•œ UIλ₯Ό μœ„ν•œ ν…ŒμŠ€νŠΈλ₯Ό μ˜λ―Έν•˜μ§€λ§Œ(3.6 μ°Έκ³ ), λ‹€λ₯Έ 의미둜 μ‹€μ œ λ°±μ—”λ“œλ₯Ό ν¬ν•¨ν•˜μ—¬ 전체 μ‹œμŠ€ν…œμ„ ν™•μž₯ν•˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό μ˜λ―Έν•©λ‹ˆλ‹€. ν›„μžμ˜ ν…ŒμŠ€νŠΈ μœ ν˜•μ€ κ΅ν™˜ μŠ€ν‚€λ§ˆμ— λŒ€ν•œ 잘λͺ»λœ μ΄ν•΄λ‘œ 인해 λ°œμƒν•  수 μžˆλŠ” ν”„λ‘ νŠΈμ—”λ“œμ™€ λ°±μ—”λ“œκ°„μ˜ 톡합 버그λ₯Ό μ»€λ²„ν•˜κΈ° λ•Œλ¬Έμ— μƒλ‹Ήνžˆ μœ μš©ν•©λ‹ˆλ‹€. λ˜ν•œ λ°±μ—”λ“œκ°„ 톡합 문제(예 : λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ Aκ°€ λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€ B에 잘λͺ»λœ λ©”μ‹œμ§€λ₯Ό 보낸닀)λ₯Ό λ°œκ²¬ν•˜κ³  배포 μ‹€νŒ¨λ₯Ό κ°μ§€ν•˜λŠ” 효과적인 λ°©λ²•μž…λ‹ˆλ‹€. Cypress와 Pupeteer같은 UI ν”„λ ˆμž„μ›Œν¬λ§ŒνΌ μΉœμˆ™ν•˜κ³  μ„±μˆ™ν•œ E2E ν…ŒμŠ€νŠΈ λ°±μ—”λ“œ ν”„λ ˆμž„μ›Œν¬λŠ” μ—†μŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ ν…ŒμŠ€νŠΈμ˜ 단점은 ꡬ성 μš”μ†Œκ°€ λ§Žμ€ ν™˜κ²½μ„ κ΅¬μ„±ν•˜λŠ” 데 λ“œλŠ” 높은 λΉ„μš©κ³Ό 주둜 λΆˆμ•ˆμ •μ„± μž…λ‹ˆλ‹€ - 50개의 λ§ˆμ΄ν¬λ‘œμ„œλΉ„μŠ€κ°€ μ œκ³΅λ˜λŠ”λ°, ν•˜λ‚˜κ°€ μ‹€νŒ¨ν•˜λ”λΌλ„ 전체 E2Eκ°€ μ‹€νŒ¨. λ”°λΌμ„œ 이 기법을 적절히 μ‚¬μš©ν•΄μ•Ό ν•˜λ©°, κ·Έ 쀑 1~10개 μ •λ„λ§Œ μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. 즉, μ†Œμˆ˜μ˜ E2E ν…ŒμŠ€νŠΈ 일지라도 배포 및 톡합 였λ₯˜μ™€ 같은 μœ ν˜•μ˜ 문제λ₯Ό μž‘μ„ 수 μžˆμŠ΅λ‹ˆλ‹€. ν”„λ‘œλ•μ…˜κ³Ό 같은 μŠ€ν…Œμ΄μ§• ν™˜κ²½μ—μ„œ μ‹€ν–‰ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: UIλŠ” λ°±μ—”λ“œμ—μ„œ λ¦¬ν„΄ν•˜λŠ” νŽ˜μ΄λ‘œλ“œκ°€ μ˜ˆμƒκ³Ό 맀우 λ‹€λ₯΄λ‹€λŠ” 것을 μ•Œμ•„μ°¨λ¦¬κΈ° μœ„ν•˜μ—¬ κΈ°λŠ₯ ν…ŒμŠ€νŠΈμ— λ§Žμ€ 투자λ₯Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

βšͺ ️ 3.8 둜그인 자격 증λͺ…을 μž¬μ‚¬μš©ν•˜μ—¬ E2E ν…ŒμŠ€νŠΈ 속도 ν–₯상

βœ… μ΄λ ‡κ²Œ 해라: μ‹€μ œ λ°±μ—”λ“œλ₯Ό ν¬ν•¨ν•˜κ³  API 호좜이 κ°€λŠ₯ν•œ μ‚¬μš©μž 토큰을 μ‚¬μš©ν•˜λŠ” E2E ν…ŒμŠ€νŠΈμ—μ„œ, λͺ¨λ“  μš”μ²­μ—μ„œ μ‚¬μš©μžκ°€ μƒμ„±λ˜κ³  둜그인이 λ˜λŠ” μˆ˜μ€€μœΌλ‘œ ν…ŒμŠ€νŠΈλ₯Ό κ²©λ¦¬ν•˜λŠ” 것은 μ•„λ‹™λ‹ˆλ‹€. λŒ€μ‹  ν…ŒμŠ€νŠΈκ°€ μ‹œμž‘λ˜κΈ° 전에 ν•œ 번만 λ‘œκ·ΈμΈν•˜κ³ (즉, before-all), 토큰을 둜컬 μ €μž₯μ†Œμ— μ €μž₯ν•΄μ„œ μ—¬λŸ¬ μš”μ²­μ— μž¬μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. 이것은 핡심 ν…ŒμŠ€νŠΈ 원칙 쀑 ν•˜λ‚˜λ₯Ό μœ„λ°˜ν•˜λŠ” 것 κ°™μŠ΅λ‹ˆλ‹€ - λ¦¬μ†ŒμŠ€ μ»€ν”Œλ§ 없이 ν…ŒμŠ€νŠΈλ₯Ό 자율적으둜 μœ μ§€ν•˜μ‹­μ‹œμ˜€. 이것은 μš°λ €ν•  만 ν•˜μ§€λ§Œ E2E ν…ŒμŠ€νŠΈμ—μ„œ μ„±λŠ₯은 핡심 관심사이며, κ°œλ³„ ν…ŒμŠ€νŠΈλ₯Ό μ‹œμž‘ν•˜κΈ° 전에 맀번 1~3개의 API μš”μ²­μ„ λ³΄λ‚΄κ²Œ 되면 μ‹€ν–‰ μ‹œκ°„μ΄ λ”μ§ν•΄μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€. 자격 증λͺ…을 μž¬μ‚¬μš©ν•œλ‹€κ³ ν•΄μ„œ ν…ŒμŠ€νŠΈκ°€ λ™μΌν•œ μ‚¬μš©μž λ ˆμ½”λ“œμ— λŒ€ν•΄ μˆ˜ν–‰λ˜μ–΄μ•Ό ν•œλ‹€λŠ” μ˜λ―ΈλŠ” μ•„λ‹™λ‹ˆλ‹€. μ‚¬μš©μž λ ˆμ½”λ“œ(예: ν…ŒμŠ€νŠΈ μœ μ € 결제 λ‚΄μ—­)에 μ˜μ‘΄ν•˜λŠ” 경우 ν•΄λ‹Ή λ ˆμ½”λ“œλ₯Ό ν…ŒμŠ€νŠΈμ˜ μΌλΆ€λ‘œ μƒμ„±ν•˜κ³  λ‹€λ₯Έ ν…ŒμŠ€νŠΈμ™€μ˜ 곡유λ₯Ό ν”Όν•˜μ‹­μ‹œμ˜€. λ˜ν•œ λ°±μ—”λ“œκ°€ μœ„μ‘°λ  수 μžˆμŒμ„ κΈ°μ–΅ν•˜μ‹­μ‹œμ˜€ - ν…ŒμŠ€νŠΈκ°€ ν”„λ‘ νŠΈμ—”λ“œμ— 쀑점을 λ‘” 경우, ν…ŒμŠ€νŠΈλ₯Ό λΆ„λ¦¬ν•˜κ³  λ°±μ—”λ“œ APIλ₯Ό μŠ€ν…ν•˜λŠ” 것이 μ’‹μŠ΅λ‹ˆλ‹€.(3.6 μ°Έκ³ )

❌ 그렇지 μ•ŠμœΌλ©΄: 200개의 ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€κ°€ μ£Όμ–΄μ‘Œκ³  λ‘œκ·ΈμΈμ— 100msκ°€ μ†Œμš”λœλ‹€κ³  κ°€μ •ν•˜λ©΄ 맀번 λ‘œκ·ΈμΈμ—λ§Œ 20μ΄ˆκ°€ μ†Œμš”λœλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: before-eachκ°€ μ•„λ‹Œ before-all에 둜그인

let authenticationToken;

// λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ μ‹€ν–‰λ˜κΈ° 전에 λ°œμƒ
before(() => {
  cy.request('POST', 'http://localhost:3000/login', {
    username: Cypress.env('username'),
    password: Cypress.env('password'),
  .then((responseFromLogin) => {
    authenticationToken = responseFromLogin.token;

// 각 ν…ŒμŠ€νŠΈ 전에 λ°œμƒ
beforeEach(setUser => () {
  cy.visit('/home', {
    onBeforeLoad (win) {
      win.localStorage.setItem('token', JSON.stringify(authenticationToken))

βšͺ ️ 3.9 μ‚¬μ΄νŠΈ 전체 νŽ˜μ΄μ§€λ₯Ό ν…ŒμŠ€νŠΈ ν•  수 μžˆλŠ” E2E smoke ν…ŒμŠ€νŠΈλ₯Ό λ§Œλ“œμ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: μš΄μ˜μ€‘μΈ μ„œλΉ„μŠ€λ₯Ό λͺ¨λ‹ˆν„°λ§ ν•˜κ±°λ‚˜ κ°œλ°œμ€‘μ—λ„ 전체 νŽ˜μ΄μ§€λ₯Ό 점검할 수 μžˆλŠ” E2E ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜μ‹­μ‹œμ˜€. 이런 μœ ν˜•μ˜ ν…ŒμŠ€νŠΈλŠ” κ°„λ‹¨νžˆ μž‘μ„±ν•˜κ³  μœ μ§€λ³΄μˆ˜ ν•  수 μžˆμ§€λ§Œ 그에 λ”°λ₯Έ νš¨κ³ΌλŠ” κ±°λŒ€ν•©λ‹ˆλ‹€. κΈ°λŠ₯μ μ΄κ±°λ‚˜ λ„€νŠΈμ›Œν¬, 개발 κ΄€λ ¨ 이슈λ₯Ό λ°œκ²¬ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ‹€λ₯Έ μ’…λ₯˜μ˜ ν…ŒμŠ€νŠΈλ“€μ€ E2E 만큼 μ‹ λ’°ν•  μˆ˜λŠ” μ—†κ³  μ™„λ²½ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.(일뢀 μš΄μ˜νŒ€μ—μ„œλŠ” λ‹¨μˆœνžˆ 운영 μ„œλ²„λ‘œ ping을 ν•˜κ³ μžˆκ³ , κ°œλ°œμžλ“€μ΄ λ‹¨μˆœ κΈ°λŠ₯ ν…ŒμŠ€νŠΈλ§Œμ„ μ‹€ν–‰ν•˜μ—¬ νŒ¨ν‚€μ§•μ΄λ‚˜ 브라우져 κ΄€λ ¨ μ΄μŠˆλŠ” 확인 ν•  수 μ—†μŠ΅λ‹ˆλ‹€.) smoke ν…ŒμŠ€νŠΈλŠ” κΈ°λŠ₯ ν…ŒμŠ€νŠΈλ₯Ό λŒ€μ²΄ν•˜κΈ° λ³΄λ‹€λŠ” smoke λ°œκ²¬μ„ μœ„ν•œ λ„κ΅¬λ‘œ 볼수 μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: λͺ¨λ“  ν…ŒμŠ€νŠΈλ„ νŒ¨μŠ€ν•˜κ³  μ„œλΉ„μŠ€ ν•ΌμŠ€μ²΄ν¬λ„ 정상이라 λͺ¨λ“ κ²ƒμ΄ μ™„λ²½ν•΄ 보일수 μžˆμ§€λ§Œ, Payment componentκ°€ νŒ¨ν‚€μ§• μ΄μŠˆκ°€ μžˆμ–΄μ„œ νŽ˜μ΄μ§€ 이동 μ‹œ ν™”λ©΄ λžœλ”λ§μ΄ μ•ˆλ  수 μžˆμŠ΅λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: λͺ¨λ“  νŽ˜μ΄μ§€μ˜ smoke νƒμƒ‰ν•˜κΈ°

it('λͺ¨λ“  νŽ˜μ΄μ§€λ₯Ό smoke ν…ŒμŠ€νŠΈ ν• λ•Œ, νŽ˜μ΄μ§€λ“€μ΄ μ •μƒμ μœΌλ‘œ λ‘œλ“œ λ˜μ–΄μ•Ό ν•œλ‹€', () => {
    // Cypressλ₯Ό μ΄μš©ν•œ 예제 μž…λ‹ˆλ‹€
    // λ‹€λ₯Έ E2E λ„κ΅¬λ‘œλ„ μ‰½κ²Œ κ΅¬ν˜„μ΄ κ°€λŠ₯ν•©λ‹ˆλ‹€

βšͺ ️ 3.10 닀같이 ν˜‘μ—…κ°€λŠ₯ν•œ λ¬Έμ„œλ‘œ ν…ŒμŠ€νŠΈ λ‚΄μš©μ„ 내보내기 ν•˜μ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: App의 신뒰도λ₯Ό 높일 수 있고, ν…ŒμŠ€νŠΈλ₯Ό 톡해 또 λ‹€λ₯Έ κ°œμ„ μ˜ κΈ°νšŒκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€. ν…ŒμŠ€νŠΈ λ‚΄μš©μ€ 덜 κΈ°μˆ μ μ΄λ©΄μ„œ μ œν’ˆ/UX와 κ΄€λ ¨λœ ν‘œν˜„μœΌλ‘œ λ˜μ–΄ μžˆμ–΄μ„œ, λ‹€μ–‘ν•œ ν˜‘μ—…μžλ“€(개발자, 고객 λ“±)간에 μ˜μ‚¬μ†Œν†΅ μˆ˜λ‹¨μœΌλ‘œ μ‚¬μš©λ  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, μ–΄λ–€ ν”„λ ˆμž„μ›Œν¬λ“€μ€ λΉ„μ¦ˆλ‹ˆμŠ€ 흐름과 μ˜ˆμƒλ˜λŠ” 결과듀을 λˆ„κ΅¬λ‚˜(μŠ€ν…Œμ΄ν¬ν™€λ”, PM) 이해할 수 μžˆλŠ” μ–Έμ–΄λ‘œ ν‘œν˜„ν•˜μ—¬ 같이 ν™•μΈν•˜κ³  ν˜‘μ—… ν•  수 있게 도움을 μ£ΌλŠ” ν•„μˆ˜μ μΈ λ¬Έμ„œκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€. 고객은 μžμ‹ μ˜ μΈμˆ˜μ‘°κ±΄μ„ 같이 μ •μ˜ν•΄ λ‚˜κ°€λ©΄μ„œ ν…ŒμŠ€νŠΈλ₯Ό μž‘μ„±ν•˜κ³  이것은 κ²°κ΅­ β€˜μΈμˆ˜ ν…ŒμŠ€νŠΈβ€™ κ°€ λ©λ‹ˆλ‹€. 이게 λ°”λ‘œ BDD (behavior-driven testing) μž…λ‹ˆλ‹€. κ°€μž₯ 유λͺ…ν•œ ν”„λ ˆμž„μ›Œν¬λ‘œλŠ” Cucumber(μžλ°”ν–₯)κ°€ μžˆμŠ΅λ‹ˆλ‹€. μ•„λž˜μ˜ 예제λ₯Ό μ°Έκ³ ν•˜μ„Έμš”. 이와 λΉ„μŠ·ν•˜λ©΄λ„ μ’€ λ‹€λ₯Έ ν”„λ ˆμž„μ›Œν¬λ‘œλŠ” UI 각 μ»΄ν¬λ„ŒνŠΈμ˜ λ‹€μ–‘ν•œ μƒνƒœλ³„ μ‹€μ œ 화면을 ν™•μΈν•˜κ³  μ–΄λ–€ 경우 그런 μƒνƒœκ°€ λ Œλ”λ§ λ˜λŠ”μ§€ 확인 ν•  수 μžˆλŠ” StoryBook 이 μžˆμŠ΅λ‹ˆλ‹€.(예. Gridλ₯Ό 데이터가 μžˆλŠ” κ²½μš°μ™€ μ—†λŠ” 경우, ν•„ν„°λœ 경우둜 λ Œλ”λ§ν•΄ λ³Ό 수 μžˆμŠ΅λ‹ˆλ‹€.) 이런 것듀은 μ œν’ˆ κ΄€κ³„μžμ—κ²Œ 맀λ ₯적일 μˆ˜λ„ μžˆμ§€λ§Œ 보톡 κ°œλ°œμžλ“€μ—κ²Œ 개발 λ¬Έμ„œ 처럼 μ‚¬μš©λ©λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: ν…ŒμŠ€νŠΈ μž‘μ„±μ„ μœ„ν•΄ λ§Žμ€ 곡수λ₯Ό λ“€μ˜€μ§€λ§Œ 결과적으둜 μœ„μ™€ 같은 μž₯점을 λ†“μΉ˜μ§€ λ©λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘ μ˜¬λ°”λ₯Έ 예: cucumber-js 의 humnan μ–Έμ–΄λ₯Ό μ‚¬μš©ν•œ ν…ŒμŠ€νŠΈ μ½”λ“œ

// Cucumber λ₯Ό μ‚¬μš©ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μ„€λͺ…: 평문을 μ‚¬μš©ν•˜μ—¬ λˆ„κ΅¬λ“  μ΄ν•΄ν•˜κ³  ν˜‘μ—… ν•  수 μžˆλ‹€

Feature: Twitter new tweet
  I want to tweet something in Twitter
  Scenario: Tweeting from the home page
    Given I open Twitter home
    Given I click on "New tweet" button
    Given I type "Hello followers!" in the textbox 
    Given I click on "Submit" button
    Then I see message "Tweet saved"

πŸ‘ μ˜¬λ°”λ₯Έ 예: Storybook을 μ΄μš©ν•œ components μƒνƒœ, μž…λ ₯ 값별 visualizing

βšͺ ️ 3.11 μžλ™ν™”λœ νˆ΄μ„ μ‚¬μš©ν•˜μ—¬ μ‹œκ°μ  문제(Visual Issues)λ₯Ό 감지해라.

βœ… μ΄λ ‡κ²Œ 해라: μ»¨ν…ŒμΈ κ°€ κ²ΉμΉ˜κ±°λ‚˜ κΉ¨μ§€λŠ” λ“±μ˜ μ‹œκ°μ  λ¬Έμ œλ“€μ΄ 감지될 λ•Œ, UI 슀크린 샷을 μΊ‘μ²˜ν•˜κΈ° μœ„ν•΄ μžλ™ν™” 도ꡬλ₯Ό μ…‹μ—…ν•˜μ„Έμš”. 이λ₯Ό 톡해, μ˜¬λ°”λ₯Έ 데이터가 μ€€λΉ„ 될 뿐만 μ•„λ‹ˆλΌ μ‚¬μš©μžκ°€ νŽΈλ¦¬ν•˜κ²Œ 변경을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ κΈ°μˆ μ€ ν˜„μž¬ 널리 μ±„νƒλ˜μ§€λŠ” μ•Šμ•˜μŠ΅λ‹ˆλ‹€. 우리의 ν…ŒμŠ€νŠΈ 사고 방식은 μ—¬μ „νžˆ κΈ°λŠ₯ ν…ŒμŠ€νŠΈμ— μ˜μ‘΄ν•˜μ§€λ§Œ μ‚¬μš©μžκ°€ μ‹€μ œλ‘œ κ²½ν—˜ν•˜λŠ” 것은 μ‹œκ°μ  μš”μ†Œμ΄λ©° λ‹€μ–‘ν•œ λ””λ°”μ΄μŠ€ μœ ν˜•λ•Œλ¬Έμ— 일뢀 UI 버그듀은 κ°„κ³Όλ˜κΈ° μ‰½μŠ΅λ‹ˆλ‹€. 일뢀 무료 νˆ΄λ“€μ€ μœ‘μ•ˆ 검사λ₯Ό μœ„ν•œ 슀크린 샷을 μƒμ„±ν•˜κ±°λ‚˜ μ €μž₯ν•˜λŠ” κΈ°λŠ₯κ³Ό 같은 기본적인 κΈ°λŠ₯듀을 μ œκ³΅ν•©λ‹ˆλ‹€. 이 방법은 μž‘μ€ 크기의 Appμ—λŠ” μΆ©λΆ„ν•˜μ§€λ§Œ, 변경이 λ°œμƒν•  λ•Œλ§ˆλ‹€ μ‚¬λžŒμ˜ 손길이 ν•„μš”ν•œ λ‹€λ₯Έ μˆ˜λ™ ν…ŒμŠ€νŠΈλ₯Ό μˆ˜ν–‰ν•˜κΈ°μ—λŠ” μ œμ•½μ΄ μžˆμŠ΅λ‹ˆλ‹€. λ°˜λ©΄μ—, λͺ…ν™•ν•œ μ •μ˜κ°€ μ—†κΈ° λ•Œλ¬Έμ— UI 문제λ₯Ό μžλ™μœΌλ‘œ κ°μ§€ν•˜λŠ” 것은 μƒλ‹Ήνžˆ μ–΄λ €μš΄ μΌμž…λ‹ˆλ‹€. - 이 뢀뢄이 Visual Regression ν…ŒμŠ€νŠΈ μ˜μ—­μž…λ‹ˆλ‹€. 이전 λ²„μ „μ˜ UIλ₯Ό 졜근 λ³€κ²½κ³Ό λΉ„κ΅ν•˜μ—¬ 차이점을 κ°μ§€ν•˜μ—¬ 문제λ₯Ό ν•΄κ²°ν•©λ‹ˆλ‹€. 일뢀 μ˜€ν”ˆμ†ŒμŠ€/무료 νˆ΄λ“€μ€ 이 κΈ°λŠ₯λ“€μ˜ 일뢀λ₯Ό μ œκ³΅ν•΄ μ£Όμ§€λ§Œ(예. wraith, PhantomCSS) μƒλ‹Ήν•œ μ…‹μ—… μ‹œκ°„μ΄ ν•„μš”ν•©λ‹ˆλ‹€. μƒμš© νˆ΄λ“€μ€(예. Applitools, μ„€μΉ˜κ°€ κ°„νŽΈν•˜κ³  관리 UI, μ•ŒλžŒ, β€˜μ‹œκ°μ  λ…Έμ΄μ¦ˆ(예. κ΄‘κ³ , μ• λ‹ˆλ©”μ΄μ…˜)’λ₯Ό μ œκ±°ν•˜λŠ” 슀마트 캑쳐와 문제λ₯Ό μΌμœΌν‚€λŠ” DOM/css의 κ·Όλ³Έ 원인을 λΆ„μ„ν•˜λŠ” κ³ κΈ‰ κΈ°λŠ₯듀을 μ œκ³΅ν•©λ‹ˆλ‹€.

❌ Otherwise: How good is a content page that display great content (100% tests passed), loads instantly but half of the content area is hidden?

✏ μ½”λ“œ 예제

πŸ‘Ž Anti Pattern Example: A typical visual regression - right content that is served badly

alt text

πŸ‘ Doing It Right Example: Configuring wraith to capture and compare UI snapshots

​# Add as many domains as necessary. Key will act as a label​

  english: ""​

​# Type screen widths below, here are a couple of examples​


  - 600​
  - 768​
  - 1024​
  - 1280​

​# Type page URL paths below, here are a couple of examples​
    path: /about
    selector: '.about'​
      selector: '.subscribe'​
    path: /subscribe

πŸ‘ Doing It Right Example: Using Applitools to get snapshot comaprison and other advanced features

import  *  as todoPage from  '../page-objects/todo-page';

describe('visual validation',  ()  =>  {

before(()  =>  todoPage.navigate());

beforeEach(()  =>  cy.eyesOpen({ appName:  'TAU TodoMVC'  }));

afterEach(()  =>  cy.eyesClose());


it('should look good',  ()  =>  {

cy.eyesCheckWindow('empty todo list');


todoPage.addTodo('Clean room');


todoPage.addTodo('Learn javascript');


cy.eyesCheckWindow('two todos');




cy.eyesCheckWindow('mark as completed');



μ„Ήμ…˜ 4️⃣: ν…ŒμŠ€νŠΈ 효과 μΈ‘μ •

βšͺ ️ 4.1 μžμ‹ κ°μ„ 갖기에 μΆ©λΆ„ν•œ 컀버리지λ₯Ό ν™•λ³΄ν•˜μ‹­μ‹œμ˜€. ~80%κ°€ 이상적인 것 κ°™μŠ΅λ‹ˆλ‹€.

βœ… μ΄λ ‡κ²Œ 해라: ν…ŒμŠ€νŠΈμ˜ λͺ©μ μ€ λΉ λ₯Έ 변경에 λŒ€ν•œ μΆ©λΆ„ν•œ μžμ‹ κ°μ„ κ°–κΈ° μœ„ν•œ κ²ƒμž…λ‹ˆλ‹€. λΆ„λͺ…νžˆ 더 λ§Žμ€ μ½”λ“œκ°€ ν…ŒμŠ€νŠΈ 될수둝 νŒ€μ€ 더 μžμ‹ κ°μ„ κ°€μ§ˆ 수 μžˆμŠ΅λ‹ˆλ‹€. μ»€λ²„λ¦¬μ§€λŠ” μ–Όλ§ˆλ‚˜ λ§Žμ€ 라인(브랜치, ꡬ문(statements) λ“±)이 ν…ŒμŠ€νŠΈμ— μ˜ν•΄ μ»€λ²„λ˜μ—ˆλŠ”μ§€μ— λŒ€ν•œ μ§€ν‘œμž…λ‹ˆλ‹€. κ·Έλ ‡λ‹€λ©΄ μ–΄λŠ 정도가 μΆ©λΆ„ν• κΉŒμš”? 10–30%λŠ” λΉŒλ“œ 정확성에 λŒ€ν•΄ νŒλ‹¨ν•˜κΈ°μ—λŠ” λΆ„λͺ…νžˆ λ„ˆλ¬΄ λ‚μŠ΅λ‹ˆλ‹€. λ°˜λ©΄μ— 100%λŠ” λΉ„μš©μ΄ 많이 λ“€κ³  μ •μž‘ λ‹Ήμ‹ μ˜ 관심을 μ€‘μš”ν•œ 뢀뢄이 μ•„λ‹Œ ν…ŒμŠ€νŠΈ μ½”λ“œλ‘œ μ˜κ²¨λ²„릴지도 λͺ¨λ¦…λ‹ˆλ‹€. 이것에 λŒ€ν•œ 닡은 μˆ˜μΉ˜λŠ” μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μœ ν˜•κ³Ό 같은 λ‹€μ–‘ν•œ μš”μ†Œλ“€μ— 따라 λ‹¬λΌμ§„λ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€. - λ§Œμ•½ 당신이 Airbus A380의 μ°¨μ„ΈλŒ€ 버전을 λ§Œλ“€λ©΄ 100%둜 λ§žμΆ°μ•Ό ν•˜μ§€λ§Œ μ›Ήνˆ° μ‚¬μ΄νŠΈλΌλ©΄ 50%λ©΄ μΆ©λΆ„ν•©λ‹ˆλ‹€. 비둝 ν…ŒμŠ€νŠΈμ— 열성인 λŒ€λΆ€λΆ„μ˜ μ‚¬λžŒλ“€μ€ μ μ ˆν•œ 컀버리지 μž„κ³„κ°’μ΄ 상황에 따라 달라져야 ν•œλ‹€κ³  ν•˜μ§€λ§Œ, κ·Έλ“€ 쀑 λŒ€λΆ€λΆ„μ€ λŒ€λ‹€μˆ˜μ˜ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ λ§Œμ‘±ν•˜κΈ° μœ„ν•΄μ„œ κ²½ν—˜μƒμœΌλ‘œ 80%(λ§ˆν‹΄ 파울러: β€œin the upper 80s or 90s”)κ°€ μ μ ˆν•˜λ‹€κ³  μ–˜κΈ°ν•©λ‹ˆλ‹€.

κ΅¬ν˜„ 팁: λ‹Ήμ‹ μ˜ CI ν™˜κ²½μ—μ„œ 컀버리지 μž„κ³„μΉ˜λ₯Ό μ„€μ •ν•˜μ—¬ κ·Έ 기쀀에 λ―ΈμΉ˜μ§€ λͺ»ν•˜λ©΄ λΉŒλ“œλ₯Ό λ©ˆμΆ”λ„λ‘ ν•˜κ³  싢을 κ²ƒμž…λ‹ˆλ‹€. (μ»΄ν¬λ„ŒνŠΈ λ‹Ή μž„κ³„μΉ˜λ₯Ό μ„€μ •ν•˜λŠ” 것도 κ°€λŠ₯ν•©λ‹ˆλ‹€. μ•„λž˜ 예제 μ½”λ“œλ₯Ό λ³΄μ„Έμš”). 이 μœ„μ—, λΉŒλ“œ 컀버리지 κ°μ†Œμ— λŒ€ν•œ 감지도 κ³ λ €ν•΄ λ³΄μ„Έμš”. (μƒˆλ‘œ 컀밋 된 μ½”λ“œκ°€ 컀버리지에 λͺ» λ―ΈμΉ  λ•Œ) - μ΄λ ‡κ²Œ ν•¨μœΌλ‘œμ¨ κ°œλ°œμžλ“€μ΄ 컀버리지λ₯Ό μ˜¬λ¦¬κ±°λ‚˜ 적어도 μœ μ§€ν•˜λ„λ‘ μ••λ°•ν•  수 μžˆμŠ΅λ‹ˆλ‹€. λ§ν•œλŒ€λ‘œ μ»€λ²„λ¦¬μ§€λŠ” 였직 ν•˜λ‚˜μ˜ 양적 μ§€ν‘œμΌ 뿐 ν…ŒμŠ€νŠΈμ˜ 견고성을 λ‚˜νƒ€λ‚΄κΈ°μ—λŠ” μΆ©λΆ„ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 그리고 λ‹€μŒ ν•­λͺ©μ— λ‚˜μ™€μžˆλŠ” κ²ƒμ²˜λŸΌ 당신을 속일 수 μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: Confidence and numbers go hand in hand, without really knowing that you tested most of the systemβ€Šβ€”β€Šthere will also be some fear. and fear will slow you down

✏ μ½”λ“œ 예제

πŸ‘ 예제: 일반적인 컀버리지 λ³΄κ³ μ„œ

alt text

πŸ‘ μ˜¬λ°”λ₯Έ 예: μ»΄ν¬λ„ŒνŠΈ λ‹Ή 컀버리지λ₯Ό μ„€μ •ν•˜μ‹­μ‹œμ˜€. (Jestλ₯Ό μ‚¬μš©ν•˜μ—¬)

![alt text](assets/bp-18-code-coverage2.jpeg "Setting up coverage per component (using Jest)

βšͺ ️ 4.2 컀버리지 리포트λ₯Ό ν™•μΈν•˜μ—¬ ν…ŒμŠ€νŠΈ λ˜μ§€ μ•Šμ€ λΆ€λΆ„κ³Ό 기타 μ΄μƒν•œ 점듀을 κ°μ§€ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: 일뢀 λ¬Έμ œλ“€μ€ λ ˆμ΄λ”λ§ μ•„λž˜λ‘œ μˆ¨μ–΄λ²„λ € 기쑴의 νˆ΄λ“€μ„ μ‚¬μš©ν•˜μ—¬ μ°ΎκΈ° 맀우 μ–΄λ ΅μŠ΅λ‹ˆλ‹€. 이것듀은 μ‹€μ œλ‘œ λ²„κ·ΈλŠ” μ•„λ‹ˆμ§€λ§Œ μ‹¬κ°ν•œ 영ν–₯을 쀄 수 μžˆλŠ” 생각지 λͺ» ν•œ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ λ™μž‘λ“€μž…λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 일뢀 μ½”λ“œ μ˜μ—­μ€ μ ˆλŒ€ λ˜λŠ” 거의 ν˜ΈμΆœλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. - β€˜PricingCalculatorβ€™λΌλŠ” μƒν’ˆ 가격을 μ„€μ •ν•˜λŠ” ν΄λž˜μŠ€κ°€ μžˆλ‹€κ³  생각해 λ³΄μ„Έμš”. DB에 100000개의 μƒν’ˆμ΄ 있고 νŒλ§€λ„ λ§Žμ§€λ§Œ 이 ν΄λž˜μŠ€λŠ” μ‹€μ œλ‘œ μ ˆλŒ€ ν˜ΈμΆœλ˜μ§€ μ•ŠλŠ” κ²ƒμœΌλ‘œ λ°ν˜€μ‘ŒμŠ΅λ‹ˆλ‹€... μ½”λ“œ 컀버리지 리포트λ₯Ό 톡해 μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ΄ 당신이 μ›ν•˜λŠ” λŒ€λ‘œ λ™μž‘ν•˜λŠ”μ§€ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. κ·Έ 외에도 λ¦¬ν¬νŠΈλŠ” μ–΄λ–€ μ½”λ“œλ“€μ΄ ν…ŒμŠ€νŠΈλ˜μ§€ μ•Šμ•˜λŠ”μ§€λ₯Ό κ°•μ‘°ν•΄μ„œ 보여쀄 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€. - μ½”λ“œμ˜ 80%κ°€ ν…ŒμŠ€νŠΈ λ˜μ—ˆλ‹€λŠ” μ•Œλ¦Όμ΄ μ€‘μš”ν•œ 뢀뢄이 μ»€λ²„λ˜μ—ˆλŠ”μ§€μ— λŒ€ν•œ μ—¬λΆ€λ₯Ό λ‚˜νƒ€λ‚΄μ§„ μ•ŠμŠ΅λ‹ˆλ‹€. 리포트λ₯Ό λ§Œλ“œλŠ” 것은 μ‰½μŠ΅λ‹ˆλ‹€. - 운영 λ˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό ν•  λ•Œ 컀버리지 νŠΈλž˜ν‚Ήμ„ ν•˜λ©΄μ„œ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μ‹€ν–‰ν•˜μ„Έμš”. 그러고 λ‚˜μ„œ 각 μ½”λ“œ μ˜μ—­μ΄ μ–Όλ§ˆλ‚˜ 자주 ν˜ΈμΆœλλŠ”μ§€λ₯Ό λ‚˜νƒ€λ‚΄λŠ” ν˜•ν˜•μƒ‰μƒ‰μ˜ 리포트λ₯Ό λ³΄μ„Έμš”. 잠깐 μ‹œκ°„μ„ λ‚΄μ„œ 이 데이터듀을 보면 λͺ‡ 가지 λ¬Έμ œμ λ“€μ„ λ°œκ²¬ν•˜κ²Œ 될 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: μ–΄λ–€ μ½”λ“œκ°€ ν…ŒμŠ€νŠΈλ˜μ§€ μ•Šμ•˜λŠ”μ§€ μ•Œ 수 μ—†μœΌλ©΄ 문제의 원인도 μ•Œ 수 μ—†μŠ΅λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: 이 컀버리지 λ¦¬ν¬νŠΈμ—λŠ” μ–΄λ–€ λ¬Έμ œκ°€ μžˆλ‚˜μš”? ν˜„μ‹€ 세계 μ‹œλ‚˜λ¦¬μ˜€λ‘œ QAμ—μ„œ μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜ μ‚¬μš©μ„ μΆ”μ ν–ˆκ³  ν₯미둜운 둜그인 νŒ¨ν„΄μ„ μ°Ύμ•˜μŠ΅λ‹ˆλ‹€. (힌트: 둜그인 μ‹€νŒ¨ νšŸμˆ˜κ°€ λΉ„λ‘€ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. λΆ„λͺ…νžˆ 무언가 잘λͺ»λ˜μ—ˆμŠ΅λ‹ˆλ‹€.) λ§ˆμΉ¨λ‚΄ 일뢀 ν”„λ‘ νŠΈμ—”λ“œ 버그가 λ°±μ—”λ“œ 둜그인 APIλ₯Ό 계속 ν˜ΈμΆœν•˜κ³  μžˆλ‹€λŠ” 것이 λ°ν˜€μ‘ŒμŠ΅λ‹ˆλ‹€.

![alt text](assets/bp-19-coverage-yoni-goldberg-nodejs-consultant.png "What’s wrong with this coverage report? based on a real-world scenario where we tracked our application usage in QA and find out interesting login patterns (Hint: the amount of login failures is non-proportional, something is clearly wrong. Finally it turned out that some frontend bug keeps hitting the backend login API)

βšͺ ️ 4.3 mutation ν…ŒμŠ€νŠΈλ₯Ό μ‚¬μš©ν•˜μ—¬ 논리적인 λ²”μœ„λ₯Ό μΈ‘μ •

βœ… μ΄λ ‡κ²Œ 해라: 전톡적인 컀버리지 츑정은 κ±°μ§“λ§μŸμ΄: 100%의 μ½”λ“œ 컀버리지λ₯Ό ν‘œμ‹œν•  수 μžˆμ§€λ§Œ, ν•¨μˆ˜ 쀑 μ˜¬λ°”λ₯Έ 응닡을 λ°˜ν™˜ν•˜λŠ” κΈ°λŠ₯은 μ—†μŠ΅λ‹ˆλ‹€. 심지어 ν•˜λ‚˜λ„. μ–΄μ°Œν•˜μ—¬? ν…ŒμŠ€νŠΈκ°€ λ°©λ¬Έν•œ μ½”λ“œ 라인을 λ‹¨μˆœν•˜κ²Œ μΈ‘μ •ν•˜μ§€λ§Œ, ν…ŒμŠ€νŠΈμ—μ„œ μ‹€μ œλ‘œ ν…ŒμŠ€νŠΈ(μ˜¬λ°”λ₯Έ 응닡을 assertion) ν•œ 것이 μžˆλŠ”μ§€ ν™•μΈν•˜μ§€λŠ” μ•ŠμŠ΅λ‹ˆλ‹€. 좜μž₯을 μœ„ν•΄ μ—¬ν–‰ν•˜κ³  μ—¬κΆŒ μŠ€ν…œν”„λ₯Ό λ³΄μ—¬μ£ΌλŠ” μ‚¬λžŒμ²˜λŸΌ - 이것은 단지 곡항과 ν˜Έν…”μ„ λ°©λ¬Έν–ˆμ„ 뿐, 일을 ν–ˆλŠ”μ§€ μ–΄λ–€ 것도 증λͺ…ν•˜μ§€ λͺ»ν•œλ‹€.

mutation 기반의 ν…ŒμŠ€νŠΈλŠ” λ‹¨μˆœν•œ 'λ°©λ¬Έ'이 μ•„λ‹Œ μ‹€μ œλ‘œ ν…ŒμŠ€νŠΈ '된' μ½”λ“œμ˜ 양을 μΈ‘μ •ν•˜λŠ”λ° 도움이 λ©λ‹ˆλ‹€. StrykerλŠ” mutation ν…ŒμŠ€νŠΈλ₯Ό μœ„ν•œ JavaScript 라이브러리이며 κ΅¬ν˜„μ΄ 정말 κΉ”λ”ν•©λ‹ˆλ‹€:

(1) μ˜λ„μ μœΌλ‘œ μ½”λ“œλ₯Ό λ³€κ²½ν•˜κ³  "버그λ₯Ό μ‹¬μŠ΅λ‹ˆλ‹€". 예λ₯Ό λ“€λ©΄, newOrder.price === 0 λŠ” newOrder.price != 0이 λ©λ‹ˆλ‹€. 이 "버그"λ₯Ό mutation이라고 ν•©λ‹ˆλ‹€.

(2) λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ μ„±κ³΅ν•˜λ©΄ μš°λ¦¬λŠ” λ¬Έμ œκ°€ μžˆλ‹€ - ν…ŒμŠ€νŠΈλŠ” 버그λ₯Ό λ°œκ²¬ν•˜λŠ” λͺ©μ μ„ λ‹¬μ„±ν•˜μ§€ λͺ»ν–ˆκ³ , mutation은 μ‚΄μ•„λ‚¨μ•˜λ‹€. ν…ŒμŠ€νŠΈκ°€ μ‹€νŒ¨ν•˜λ©΄ μ—„μ²­λ‚œ mutation이 μ£½μ—ˆλ‹€.

λͺ¨λ“  ν˜Ήμ€ λŒ€λΆ€λΆ„μ˜ mutation이 μ£½μ—ˆλ‹€λŠ” 것을 μ•Œλ©΄ 전톡적인 컀버리지보닀 훨씬 더 높은 μ‹ λ’°λ₯Ό 얻을 수 있으며 ꡬ성 μ‹œκ°„μ€ λΉ„μŠ·ν•©λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄:οΏ½ 85%의 μ»€λ²„λ¦¬μ§€λŠ” ν…ŒμŠ€νŠΈμ—μ„œ μ½”λ“œμ˜ 85%μ—μ„œ 버그λ₯Ό κ°μ§€ν•œλ‹€λŠ” μ˜λ―Έμž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예:οΏ½ 100% 컀버리지, 0% ν…ŒμŠ€νŠΈ

function addNewOrder(newOrder) {
    logger.log(`Adding new order ${newOrder}`);;
    Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`);

    return {approved: true};

it("Test addNewOrder, don't use such test names", () => {
    addNewOrder({asignee: "",price: 120});
}); // 100% 컀버리지가 λ‚˜μ˜€μ§€λ§Œ 아무것도 ν™•μΈν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

πŸ‘ μ˜¬λ°”λ₯Έ 예: mutation ν…ŒμŠ€νŠΈ 도ꡬ인 Stryker λ³΄κ³ μ„œλŠ” ν…ŒμŠ€νŠΈ λ˜μ§€ μ•Šμ€ μ½”λ“œμ˜ 양을 κ°μ§€ν•˜κ³  κ³„μ‚°ν•©λ‹ˆλ‹€.

alt text

βšͺ ️ 4.4 ν…ŒμŠ€νŠΈ λ¦°ν„°λ‘œ ν…ŒμŠ€νŠΈ μ½”λ“œ 문제 방지

βœ… μ΄λ ‡κ²Œ 해라: ESLint ν”ŒλŸ¬κ·ΈμΈ μ„ΈνŠΈλŠ” ν…ŒμŠ€νŠΈ μ½”λ“œ νŒ¨ν„΄μ„ κ²€μ‚¬ν•˜κ³  문제λ₯Ό λ°œκ²¬ν•˜κΈ° μœ„ν•΄ νŠΉλ³„νžˆ μ œμž‘λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄ eslint-plugin-mochaλŠ” ν…ŒμŠ€νŠΈκ°€ κΈ€λ‘œλ²Œ μˆ˜μ€€μ—μ„œ μž‘μ„±λ  λ•Œ(describe() λ¬Έ μ•„λž˜μ— μžˆμ§€ μ•ŠμŒ) λ˜λŠ” ν…ŒμŠ€νŠΈλ₯Ό κ±΄λ„ˆ λ›°κ³  λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όλ˜μ—ˆλ‹€λŠ” 잘λͺ»λœ λ―ΏμŒμ„ κ°€μ§ˆ λ•Œ κ²½κ³ ν•©λ‹ˆλ‹€. μœ μ‚¬ν•˜κ²Œ, eslint-plugin-jestλŠ” 예λ₯Ό λ“€μ–΄ ν…ŒμŠ€νŠΈμ— μ•„λ¬΄λŸ° assertion이 없을 λ•Œ κ²½κ³ ν•©λ‹ˆλ‹€.(아무것도 ν™•μΈν•˜μ§€ μ•ŠμŒ)

❌ 그렇지 μ•ŠμœΌλ©΄: 90%의 μ½”λ“œ 컀버리지와 100%의 녹색 ν…ŒμŠ€νŠΈλ₯Ό 보며 λ―Έμ†Œμ§“λŠ” 것은 λ§Žμ€ ν…ŒμŠ€νŠΈκ°€ 아무것도 assertionν•˜μ§€ μ•Šκ³  λ§Žμ€ ν…ŒμŠ€νŠΈ μŠ€μœ„νŠΈκ°€ κ±΄λ„ˆ λ›°μ–΄μ§„λ‹€λŠ” 것을 μ•Œ λ•Œ κΉŒμ§€λ§Œμž…λ‹ˆλ‹€. 이 잘λͺ»λœ κ²°κ³Όλ₯Ό λ°”νƒ•μœΌλ‘œ μ–΄λ–€ 것도 λ°°ν¬ν•˜μ§€ μ•Šμ•˜κΈ°λ₯Ό λ°”λžλ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: 였λ₯˜λ‘œ 가득 μ°¬ ν…ŒμŠ€νŠΈ μΌ€μ΄μŠ€, 운 μ’‹κ²Œλ„ λ¦°ν„°κ°€ μž‘μ•˜μŠ΅λ‹ˆλ‹€.

describe("Too short description", () => {
  const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead
  it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words

it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite
  expect("somevalue"); // error:no-assert

it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests

μ„Ήμ…˜ 5️⃣: 지속적인 톡합

βšͺ ️ 5.1 λ¦°ν„°λ₯Ό ν’μ„±ν•˜κ²Œ κ΅¬μ„±ν•˜κ³  린트 λ¬Έμ œκ°€ μžˆλŠ” λΉŒλ“œλ₯Ό μ€‘λ‹¨ν•˜μ‹­μ‹œμ˜€

βœ… μ΄λ ‡κ²Œ 해라: λ¦°ν„°λŠ” 곡짜 점심이며, 5λΆ„μ˜ μ„€μ •λ§ŒμœΌλ‘œ μ½”λ“œλ₯Ό μ§€μΌœμ£Όκ³  μž…λ ₯κ³Ό λ™μ‹œμ— μ€‘μš”ν•œ 문제λ₯Ό ν¬μ°©ν•˜λŠ” μžλ™ μ‘°μ’… μž₯치λ₯Ό κ±°μ € 얻을 수 μžˆμŠ΅λ‹ˆλ‹€. λ¦°ν„°κ°€ μž₯식(μ„Έλ―Έμ½œλ‘ )에 μ§€λ‚˜μ§€ μ•Šλ˜ μ‹œλŒ€λŠ” μ§€λ‚˜κ°”μŠ΅λ‹ˆλ‹€. μš”μ¦˜μ˜ λ¦°ν„°λŠ” μ˜¬λ°”λ₯΄κ²Œ throwλ˜μ§€ μ•Šκ³  정보가 μ†μ‹€λ˜λŠ” 였λ₯˜μ™€ 같은 μ‹¬κ°ν•œ 문제λ₯Ό 포착 ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κΈ°λ³Έ κ·œμΉ™ μ„ΈνŠΈ (ESLint standard ν˜Ήμ€ Airbnb style) μœ„μ—, 단언문이 빠진 ν…ŒμŠ€νŠΈλ₯Ό λ°œκ²¬ν•΄μ£ΌλŠ” eslint-plugin-chai-expect와 같은 νŠΉν™”λœ λ¦°ν„°λ₯Ό ν¬ν•¨ν•˜λŠ” 것을 κ³ λ €ν•˜μ‹­μ‹œμ˜€. eslint-plugin-promise λŠ” resolveλ˜μ§€ μ•ŠλŠ” Promiseλ₯Ό λ°œκ²¬ν•΄μ€λ‹ˆλ‹€ (이런 μ½”λ“œλŠ” κ³„μ†ν•΄μ„œ μ‹€ν–‰λ˜λŠ”κ²ƒμ΄ λΆˆκ°€λŠ₯ν•©λ‹ˆλ‹€), eslint-plugin-securityλŠ” DOS 곡격에 μ‚¬μš©λ  수 μžˆλŠ” μ·¨μ•½ν•œ μ •κ·œμ‹μ„ λ°œκ²¬ν•΄μ£Όλ©°, eslint-plugin-you-dont-need-lodash-underscoreλŠ” μ½”λ“œκ°€ Lodash._map(...)κ³Ό 같은 V8 μ½”μ–΄ λ©”μ†Œλ“œμ˜ 일뢀인 μœ ν‹Έλ¦¬ν‹° 라이브러리 λ©”μ†Œλ“œλ₯Ό μ‚¬μš©ν•  λ•Œ κ²½κ³ ν•΄μ€λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: ν”„λ‘œλ•μ…˜μ΄ 계속 κΉ¨μ§€λŠ”λ° λ‘œκ·Έμ— μ—λŸ¬μ˜ stack traceκ°€ ν‘œμ‹œλ˜μ§€ μ•ŠλŠ” μš°μšΈν•œ 날을 μƒμƒν•΄λ΄…μ‹œλ‹€. μ–΄λ–»κ²Œ 된 κ±ΈκΉŒμš”? μ‹€μˆ˜λ‘œ μ½”λ“œκ°€ μ—λŸ¬κ°€ μ•„λ‹Œ 객체λ₯Ό λ˜μ§€κ³  μžˆμ–΄μ„œ stack traceκ°€ μ†μ‹€λ˜μ—ˆλ‹€λ©΄, 벽에 머리λ₯Ό 듀이박기 λ”± μ’‹μ„κ²ƒμž…λ‹ˆλ‹€. 5λΆ„μ˜ λ¦°ν„° μ„€μ •μœΌλ‘œ 이런 μ˜€νƒ€λ₯Ό κ°μ§€ν•˜κ³  ν•˜λ£¨λ₯Ό μ§€μΌœλ‚Ό 수 μžˆμŠ΅λ‹ˆλ‹€.

✏ μ½”λ“œ 예제

πŸ‘Ž μ˜¬λ°”λ₯΄μ§€ μ•Šμ€ 예: 잘λͺ»λœ Error 객체가 μ‹€μˆ˜λ‘œ throwλ˜μ–΄ 이 였λ₯˜μ— λŒ€ν•œ stack traceκ°€ λ‚˜νƒ€λ‚˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 운 μ’‹κ²Œλ„ ESLintλŠ” λ‹€μŒκ³Ό 같은 ν”„λ‘œλ•μ…˜ 버그λ₯Ό μž‘μ•„λƒ…λ‹ˆλ‹€.

alt text

βšͺ ️ 5.2 둜컬 개발자 CI둜 ν”Όλ“œλ°± μ£ΌκΈ° 단좕

βœ… μ΄λ ‡κ²Œ 해라: ν…ŒμŠ€νŠΈ, 린트, 취약점 확인 λ“±κ³Ό 같은 κ·Όμ‚¬ν•œ ν’ˆμ§ˆ 검사가 ν¬ν•¨λœ CIλ₯Ό μ‚¬μš©ν•©λ‹ˆκΉŒ? κ°œλ°œμžκ°€ 이 νŒŒμ΄ν”„λΌμΈμ„ λ‘œμ»¬μ—μ„œλ„ μ‹€ν–‰ν•  수 μžˆλ„λ‘ ν•΄μ„œ ν”Όλ“œλ°± μ£ΌκΈ°λ₯Ό λ‹¨μΆ•ν•˜μ‹­μ‹œμ˜€. μ™œ? 효율적인 ν…ŒμŠ€νŠΈ ν”„λ‘œμ„ΈμŠ€λŠ” λ§Žμ€ 반볡 루프λ₯Ό κ΅¬μ„±ν•©λ‹ˆλ‹€. (1) μ‹œλ„ -> (2) ν”Όλ“œλ°± -> (3) λ¦¬νŒ©ν„°λ§. ν”Όλ“œλ°±μ΄ λΉ λ₯Όμˆ˜λ‘ κ°œλ°œμžκ°€ λͺ¨λ“ˆ 각각을 κ°œμ„ ν•˜λ©° κ²°κ³Όλ₯Ό μ™„λ²½ν•˜κ²Œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ν•œνŽΈ, ν”Όλ“œλ°±μ΄ λŠ¦μ–΄μ§€λ©΄μ„œ ν•˜λ£¨μ— κ°œμ„ μ΄ λ°˜λ³΅λ˜λŠ” λΉˆλ„κ°€ 적어진닀면, νŒ€μ€ 이미 λ‹€λ₯Έ 주제 / μž‘μ—… / λͺ¨λ“ˆλ‘œ λ„˜μ–΄κ°ˆ 수 있으며 ν•΄λ‹Ή λͺ¨λ“ˆμ˜ μˆ˜μ •μ΄ 이루어지지 μ•Šμ„ 수 μžˆμŠ΅λ‹ˆλ‹€.

μ‹€μ œλ‘œ λͺ‡λͺ‡ CI 곡급 업체 (예: CircleCI load CLI) λŠ” νŒŒμ΄ν”„λΌμΈμ˜ 둜컬 싀행을 ν—ˆμš©ν•©λ‹ˆλ‹€. wallaby와 같은 λͺ‡λͺ‡ μƒμš© λ„κ΅¬λŠ” 개발자 ν”„λ‘œν† νƒ€μž…μœΌλ‘œ 높은 κ°€μΉ˜μ˜ ν…ŒμŠ€νŠΈ 톡찰λ ₯을 μ œκ³΅ν•©λ‹ˆλ‹€(ν˜‘μ°¬ μ•„λ‹˜). λ˜λŠ” λͺ¨λ“  ν’ˆμ§ˆ κ΄€λ ¨ λͺ…λ Ήμ–΄(예 : test, lint, vulnerabilities)λ₯Ό μ‹€ν–‰ν•˜λŠ” npm 슀크립트λ₯Ό package.json에 μΆ”κ°€ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 병렬화λ₯Ό μœ„ν•΄ concurrently와 같은 도ꡬλ₯Ό μ‚¬μš©ν•˜κ³  λͺ…λ Ήμ–΄ 쀑 ν•˜λ‚˜κ°€ μ‹€νŒ¨ν•  κ²½μš°μ—λŠ” 0이 μ•„λ‹Œ μ’…λ£Œ μ½”λ“œλ₯Ό μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. 이제 κ°œλ°œμžλŠ” ν•˜λ‚˜μ˜ λͺ…령을 ν˜ΈμΆœν•΄μ•Ό ν•©λ‹ˆλ‹€. β€˜npm run quality’— 즉각적인 ν”Όλ“œλ°±μ„ λ°›μŠ΅λ‹ˆλ‹€. githook을 μ‚¬μš©ν•˜μ—¬ ν’ˆμ§ˆ 검사에 μ‹€νŒ¨ν•œ 경우 컀밋을 μ€‘λ‹¨ν•˜λŠ” 것도 κ³ λ €ν•˜μ‹­μ‹œμ˜€ (huskyκ°€ 도움될 수 있음)

❌ 그렇지 μ•ŠμœΌλ©΄: μ½”λ“œλ₯Ό μž‘μ„±ν•œ λ‹€μŒ 날에 ν’ˆμ§ˆ κ²°κ³Όκ°€ λ„μ°©ν•œλ‹€λ©΄ ν…ŒμŠ€νŠΈλŠ” 개발 과정에 μžμ—°μŠ€λŸ½κ²Œ 포함될 수 μ—†μŠ΅λ‹ˆλ‹€

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: μ½”λ“œ ν’ˆμ§ˆ 검사λ₯Ό μˆ˜ν–‰ν•˜λŠ” npm μŠ€ν¬λ¦½νŠΈλŠ” μš”μ²­ μ‹œ λ˜λŠ” κ°œλ°œμžκ°€ μƒˆ μ½”λ“œλ₯Ό ν‘Έμ‹œν•˜λ €κ³  ν•  λ•Œ λͺ¨λ‘ λ³‘λ ¬λ‘œ μ‹€ν–‰λ©λ‹ˆλ‹€.

"scripts": {
    "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"",
    "inspect:lint": "eslint .",
    "inspect:vulnerabilities": "npm audit",
    "inspect:license": "license-checker --failOn GPLv2",
    "inspect:complexity": "plato .",

    "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\""
  "husky": {
    "hooks": {
      "precommit": "npm run inspect:all",
      "prepush": "npm run inspect:all"

βšͺ ️ 5.3 μ‹€μ œ ν”„λ‘œλ•μ…˜ 미러λ₯Ό 톡해 e2e ν…ŒμŠ€νŠΈ μˆ˜ν–‰

βœ… μ΄λ ‡κ²Œ 해라: e2e ν…ŒμŠ€νŠΈλŠ” λͺ¨λ“  CI νŒŒμ΄ν”„λΌμΈμ˜ μ£Όμš” κ³Όμ œμž…λ‹ˆλ‹€. λͺ¨λ“  κ΄€λ ¨ ν΄λΌμš°λ“œ μ„œλΉ„μŠ€κ°€ λ™μΌν•œ μž„μ‹œ ν”„λ‘œλ•μ…˜ 미러λ₯Ό μ¦‰μ„μ—μ„œ μƒμ„±ν•˜λŠ” 것은 μ§€λ£¨ν•˜κ³  λΉ„μš©μ΄ 많이 λ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. 졜고의 νƒ€ν˜‘μ μ„ μ°ΎλŠ”κ²ƒμ΄ κ²Œμž„μž…λ‹ˆλ‹€. Docker-composeλ₯Ό μ‚¬μš©ν•˜λ©΄ 단일 ν…μŠ€νŠΈ 파일둜 λ™μΌν•œ μ»¨ν…Œμ΄λ„ˆλ‘œ 격리된 도컀 ν™˜κ²½μ„ λ§Œλ“€ 수 μžˆμ§€λ§Œ λ°±μ—… 기술(예: λ„€νŠΈμ›Œν‚Ή, 배포 λͺ¨λΈ)은 μ‹€μ œ ν”„λ‘œλ•μ…˜κ³Ό λ‹€λ¦…λ‹ˆλ‹€. μ‹€μ œ AWS μ„œλΉ„μŠ€ κΈ°λŠ₯κ³Ό ν•¨κ»˜ λ™μž‘ν•˜κ²Œ ν•˜κΈ° μœ„ν•΄ 이λ₯Ό 'AWS Local'κ³Ό κ²°ν•©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. Serverless와 Serverless같은 μ—¬λŸ¬ ν”„λ ˆμž„μ›Œν¬λ₯Ό μ‚¬μš©ν•˜λŠ” 경우 AWS SAM을 μ΄μš©ν•΄ λ‘œμ»¬μ—μ„œ FaaS μ½”λ“œλ₯Ό ν˜ΈμΆœν•  수 μžˆμŠ΅λ‹ˆλ‹€.

κ±°λŒ€ν•œ μΏ λ²„λ„€ν‹°μŠ€ μƒνƒœκ³„λŠ” λ§Žμ€ μƒˆλ‘œμš΄ 도ꡬ가 자주 λ‚˜μ˜€μ§€λ§Œ, 아직 둜컬 및 CI λ―ΈλŸ¬λ§μ„ μœ„ν•œ νŽΈλ¦¬ν•œ λ„κ΅¬μ˜ ν‘œμ€€μ„ κ³΅μ‹ν™”ν•˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€. ν•œκ°€μ§€ 방법은 μ‹€μ œμ™€ λΉ„μŠ·ν•˜μ§€λ§Œ μ˜€λ²„ν—€λ“œκ°€ 적은 Minikube 및 MicroK8sκ³Ό 같은 도ꡬλ₯Ό μ‚¬μš©ν•˜μ—¬ 'μ΅œμ†Œν™”λœ μΏ λ²„λ„€ν‹°μŠ€'λ₯Ό μ‹€ν–‰ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 또 λ‹€λ₯Έ 방법은 원격 'μ‹€μ œ μΏ λ²„λ„€ν‹°μŠ€'λ₯Ό ν…ŒμŠ€νŠΈν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 일뢀 CI 벀더(예: Codefresh은 μΏ λ²„λ„€ν‹°μŠ€ ν™˜κ²½κ³Ό ν†΅ν•©λ˜μ–΄ μžˆμ–΄μ„œ μ‹€μ œ μƒν™©μ—μ„œ CI νŒŒμ΄ν”„λΌμΈμ„ μ‰½κ²Œ μ‹€ν–‰ν•  수 있으며, 또 원격 μΏ λ²„λ„€ν‹°μŠ€μ— λŒ€ν•œ μ‚¬μš©μž 지정 μŠ€ν¬λ¦½νŒ…μ„ ν—ˆμš©ν•©λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: ν”„λ‘œλ•μ…˜ 및 ν…ŒμŠ€νŠΈμ— μ„œλ‘œ λ‹€λ₯Έ κΈ°μˆ μ„ μ‚¬μš©ν•˜λ©΄ 두 가지 배포 λͺ¨λΈμ˜ 관리가 ν•„μš”ν•˜κ³  κ°œλ°œμžμ™€ 운영 νŒ€μ΄ λΆ„λ¦¬λ©λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: μΏ λ²„λ„€ν‹°μŠ€ ν΄λŸ¬μŠ€ν„°λ₯Ό μ¦‰μ‹œ μƒμ„±ν•˜λŠ” CI νŒŒμ΄ν”„λΌμΈ (동적 ν™˜κ²½ μΏ λ²„λ„€ν‹°μŠ€)

stage: deploy
- kubectl create ns $NAMESPACE
- kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL"
- mkdir .generated
- sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml"
- kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml
- kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml
name: test-for-ci

βšͺ ️ 5.4 ν…ŒμŠ€νŠΈ μ‹€ν–‰μ˜ 병렬화

βœ… μ΄λ ‡κ²Œ 해라: μ œλŒ€λ‘œλ§Œ ν•˜λ©΄ ν…ŒμŠ€νŠΈλŠ” 거의 즉각적인 ν”Όλ“œλ°±μ„ μ£ΌλŠ” 24/7 μΉœκ΅¬μž…λ‹ˆλ‹€. μ‹€μ œλ‘œ 단일 μŠ€λ ˆλ“œμ—μ„œ 500개의 CPU μ œν•œ λ‹¨μœ„ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜λŠ”λ°λŠ” μ‹œκ°„μ΄ 였래 걸릴 수 μžˆμŠ΅λ‹ˆλ‹€. μš΄μ’‹κ²Œλ„ μ΅œμ‹  ν…ŒμŠ€νŠΈ λŸ¬λ„ˆμ™€ CI ν”Œλž«νΌ(예: Jest, AVA, Mocha extension)은 ν…ŒμŠ€νŠΈλ₯Ό μ—¬λŸ¬ ν”„λ‘œμ„ΈμŠ€λ‘œ λ³‘λ ¬ν™”ν•˜μ—¬ ν”Όλ“œλ°± μ‹œκ°„μ„ 크게 κ°œμ„ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 일뢀 CI λ²€λ”λŠ” ν…ŒμŠ€νŠΈλ₯Ό μ»¨ν…Œμ΄λ„ˆ κ°„ λ³‘λ ¬ν™”ν•˜μ—¬(!) ν”Όλ“œλ°± μ‹œκ°„μ„ λ”μš± λ‹¨μΆ•μ‹œν‚΅λ‹ˆλ‹€. 각각 λ‹€λ₯Έ ν”„λ‘œμ„ΈμŠ€μ—μ„œ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰(λ‘œμ»¬μ—μ„œ μ—¬λŸ¬ ν”„λ‘œμ„ΈμŠ€λ‘œ ν˜Ήμ€ μ—¬λŸ¬ 머신을 μ‚¬μš©ν•˜λŠ” 일뢀 ν΄λΌμš°λ“œ CLIλ₯Ό 톡해)ν•˜μ—¬ ν…ŒμŠ€νŠΈλ₯Ό μžλ™ν™” ν•˜μ‹­μ‹œμ˜€.

❌ 그렇지 μ•ŠμœΌλ©΄: μƒˆ μ½”λ“œλ₯Ό ν‘Έμ‰¬ν•˜κ³  이미 λ‹€μŒ κΈ°λŠ₯을 μ½”λ”© ν•  λ•Œ, (ν•œ μ‹œκ°„ 후에) ν…ŒμŠ€νŠΈ κ²°κ³Όλ₯Ό μ–»λŠ” 것은 ν…ŒμŠ€νŠΈμ˜ 관련성을 λ–¨μ–΄λœ¨λ¦¬κΈ° μœ„ν•œ ν›Œλ₯­ν•œ λ°©λ²•μž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: Mocha Parallelκ³Ό Jest λŠ” ν…ŒμŠ€νŠΈ 병렬화 덕뢄에 덕뢄에 기쑴의 Mochaλ₯Ό μ‰½κ²Œ λŠ₯κ°€ν•©λ‹ˆλ‹€. (Credit: JavaScript Test-Runners Benchmark)

alt text

βšͺ ️ 5.5 λΌμ΄μ„ΌμŠ€ 및 ν‘œμ ˆμ„ κ²€μ‚¬ν•˜μ—¬ 법적 문제λ₯Ό ν”Όν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: 라이센싱과 ν‘œμ ˆ λ¬Έμ œλŠ” λ‹Ήμž₯ λ‹Ήμ‹ μ˜ μ£Όμš” 관심사가 아닐 수 μžˆμ§€λ§Œ, 10λΆ„ μ•ˆμ— 이 λ‚΄μš©μ„ ν™•μΈν•˜μ§€ μ•ŠμœΌμ‹œκ² μŠ΅λ‹ˆκΉŒ? λ§Žμ€ npm νŒ¨ν‚€μ§€μ˜ λΌμ΄μ„ΌμŠ€ 체크 및 ν‘œμ ˆ 확인(μƒμš© λ„κ΅¬μ˜ 무료 ν”Œλžœ)을 CI νŒŒμ΄ν”„λΌμΈμ— μ‰½κ²Œ ν¬ν•¨μ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€. 그리고 μ œν•œμ μΈ λΌμ΄μ„ΌμŠ€μ˜ μ’…μ†μ„±μ΄λ‚˜ Stack Overflowμ—μ„œ λ³΅μ‚¬ν•˜μ—¬ 뢙여넣은 일뢀 μ €μž‘κΆŒμ„ μœ„λ°˜ν•œ κ²ƒμœΌλ‘œ λ³΄μ΄λŠ” μ½”λ“œλ₯Ό μ κ²€ν•˜μ‹­μ‹œμ˜€.

❌ 그렇지 μ•ŠμœΌλ©΄: κ°œλ°œμžκ°€ μ˜λ„μΉ˜ μ•Šκ²Œ λΆ€μ μ ˆν•œ λΌμ΄μ„ΌμŠ€κ°€ ν¬ν•¨λœ νŒ¨ν‚€μ§€λ₯Ό μ‚¬μš©ν•˜κ±°λ‚˜ μƒμš© μ½”λ“œλ₯Ό λ³΅μ‚¬ν•˜μ—¬ 법적 λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예:

//install license-checker in your CI environment or also locally
npm install -g license-checker

//ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build
license-checker --summary --failOn BSD

alt text

βšͺ ️ 5.6 μ·¨μ•½ν•œ 쒅속성을 μ§€μ†μ μœΌλ‘œ 검사

βœ… μ΄λ ‡κ²Œ 해라: Express와 같이 μƒλ‹Ήνžˆ ν‰νŒμ΄ 쒋은 쒅속성 쑰차도 취약점이 μžˆμŠ΅λ‹ˆλ‹€. npm auditκ³Ό 같은 컀λ€λ‹ˆν‹° 도ꡬ λ˜λŠ” snyk같은 μƒμš© 도ꡬλ₯Ό μ‚¬μš©ν•˜μ—¬ μ‰½κ²Œ μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€(무료 컀λ€λ‹ˆν‹° 버전도 제곡). λ‘˜λ‹€ λ‹Ήμ‹ μ˜ CI의 λͺ¨λ“  λΉŒλ“œμ—μ„œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: μ „μš© 도ꡬ 없이 μ½”λ“œλ₯Ό μ·¨μ•½μ μœΌλ‘œλΆ€ν„° μ•ˆμ „ν•˜κ²Œ μœ μ§€ν•˜λ €λ©΄ μƒˆλ‘œμš΄ μœ„ν˜‘μ— λŒ€ν•œ μ΅œμ‹  μ†Œμ‹μ„ μ§€μ†μ μœΌλ‘œ μ«“μ•„μ•Ό ν•΄μ„œ 맀우 μ§€λ£¨ν•©λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: NPM Audit κ²°κ³Ό

alt text

βšͺ ️ 5.7 쒅속성 μ—…λ°μ΄νŠΈ μžλ™ν™”

βœ… μ΄λ ‡κ²Œ 해라: yarnκ³Ό npm이 졜근 μ†Œκ°œν•œ package-lock.jsonλŠ” μ€‘λŒ€ν•œ λ„μ „μž…λ‹ˆλ‹€(지μ˜₯으둜 κ°€λŠ” 길은 쒋은 μ˜λ„λ‘œ 포μž₯λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€). 기본적으둜 이제 νŒ¨ν‚€μ§€λŠ” 더이상 μ—…λ°μ΄νŠΈλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 'npm install'κ³Ό 'npm update'을 톡해 μƒˆλ‘œμš΄ 배포λ₯Ό 많이 μˆ˜ν–‰ν•˜λŠ” νŒ€λ„ μƒˆλ‘œμš΄ μ—…λ°μ΄νŠΈλ₯Ό 받지 μ•ŠμŠ΅λ‹ˆλ‹€. 이둜 인해 ν•˜μœ„ 쒅속 νŒ¨ν‚€μ§€ 버전이 μ΅œμƒμ΄κ±°λ‚˜, μ΅œμ•…μ˜ κ²½μš°μ—λŠ” μ·¨μ•½ν•œ μ½”λ“œκ°€ λ©λ‹ˆλ‹€. νŒ€μ€ 이제 νŒ¨ν‚€μ§€λ₯Ό μˆ˜λ™μœΌλ‘œ μ—…λ°μ΄νŠΈν•˜κΈ° μœ„ν•΄ 개발자의 μ˜μ§€μ™€ 기얡에 μ˜μ‘΄ν•˜κ±°λ‚˜ ncu같은 도ꡬλ₯Ό μˆ˜λ™μœΌλ‘œ μ‚¬μš©ν•˜κ²Œ λ©λ‹ˆλ‹€. 보닀 μ•ˆμ •μ μΈ 방법은 κ°€μž₯ μ•ˆμ •μ μΈ 쒅속성 버전을 μ–»λŠ” ν”„λ‘œμ„ΈμŠ€λ₯Ό μžλ™ν™”ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. λ¬˜μ±…μ€ μ—†μ§€λ§Œ κ°€λŠ₯ν•œ 두가지 μžλ™ν™” 방법이 μžˆμŠ΅λ‹ˆλ‹€:

(1) CIλŠ” 'npm outdated' λ˜λŠ” 'npm-check-updates(ncu)'같은 νˆ΄μ„ μ‚¬μš©ν•˜μ—¬ 였래된 쒅속성을 가진 λΉŒλ“œλ₯Ό μ‹€νŒ¨ν•˜κ²Œ ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ·Έλ ‡κ²Œν•˜λ©΄ κ°œλ°œμžκ°€ 쒅속성 μ—…λ°μ΄νŠΈλ₯Ό ν•΄μ•Όν•©λ‹ˆλ‹€.

(2) μ½”λ“œλ₯Ό μŠ€μΊ”ν•˜κ³  μ—…λ°μ΄νŠΈλœ μ’…μ†μ„±μœΌλ‘œ PR을 μžλ™μœΌλ‘œ λ³΄λ‚΄μ£ΌλŠ” μƒμš© 도ꡬλ₯Ό μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. λ‚¨μ•„μžˆλŠ” ν₯미둜운 질문 ν•˜λ‚˜λŠ” 쒅속성 μ—…λ°μ΄νŠΈ μ •μ±…μž…λ‹ˆλ‹€. λͺ¨λ“  νŒ¨μΉ˜μ—μ„œ μ—…λ°μ΄νŠΈλ₯Ό ν•˜λ©΄ λ„ˆλ¬΄ λ§Žμ€ μ˜€λ²„ν—€λ“œκ°€ λ°œμƒν•©λ‹ˆλ‹€. 메이저 버전이 곡개될 λ•Œ λ°”λ‘œ μ—…λ°μ΄νŠΈ ν•˜λ©΄ λΆˆμ•ˆμ •ν•œ 버전을 가리킬 수 μžˆμŠ΅λ‹ˆλ‹€.(λ§Žμ€ νŒ¨ν‚€μ§€κ°€ μΆœμ‹œλœ 직후 첫 날에 μ·¨μ•½ν•œ κ²ƒμœΌλ‘œ λ°ν˜€ 짐 esline-scope사건 μ°Έμ‘°)

효율적인 μ—…λ°μ΄νŠΈ 정책은 일뢀 '투자 κΈ°κ°„'을 ν—ˆμš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 둜컬 사본을 버리기 전에 μ’…μ†μ„±μ˜ μ‹œκ°„κ³Ό 버전을 @latest보닀 μ•½κ°„ λ’€μ³μ§€κ²Œ ν•˜μ‹­μ‹œμ˜€. (예: 둜컬 버전은 1.3.1 λ ˆνŒŒμ§€ν† λ¦¬ 버전은 1.3.8)

❌ 그렇지 μ•ŠμœΌλ©΄: λ‹Ήμ‹ μ˜ μ œν’ˆμ€ μž‘μ„±μžκ°€ μœ„ν—˜ν•˜λ‹€κ³  λͺ…μ‹œμ μœΌλ‘œ νƒœκ·Έν•œ νŒ¨ν‚€μ§€λ₯Ό μ‹€ν–‰ν•  κ²ƒμž…λ‹ˆλ‹€.

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: μ½”λ“œκ°€ μ΅œμ‹  버전보닀 μ–΄λŠμ •λ„ λ’€μ³μ§€λŠ”μ§€ κ°μ§€ν•˜κΈ° μœ„ν•˜μ—¬ ncuλ₯Ό μˆ˜λ™μœΌλ‘œ λ˜λŠ” CI νŒŒμ΄ν”„λΌμΈ λ‚΄μ—μ„œ μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

alt text

βšͺ ️ 5.8 기타, λ…Έλ“œμ™€ κ΄€λ ¨μ—†λŠ” CI 팁

βœ… μ΄λ ‡κ²Œ 해라: 이 글은 Node.js와 관련이 μžˆκ±°λ‚˜ μ΅œμ†Œν•œ Node.js둜 예λ₯Ό λ“€ 수 μžˆλŠ” ν…ŒμŠ€νŠΈ 쑰언에 쀑점을 두고 μžˆμŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜ μ΄λ²ˆμ—λŠ” Node.js와 κ΄€λ ¨μ—†μ§€λ§Œ 잘 μ•Œλ €μ§„ 팁 λͺ‡κ°œλ₯Ό κ·Έλ£Ήν™” ν•˜μ˜€μŠ΅λ‹ˆλ‹€.

  1. 선언적 ꡬ문을 μ‚¬μš©ν•˜μ‹­μ‹œμ˜€. λŒ€λΆ€λΆ„μ˜ λ²€λ”μ—μ„œλŠ” 선택할 수 μ—†μ§€λ§Œ, 이전 λ²„μ „μ˜ Jenkinsμ—μ„œ μ½”λ“œ λ˜λŠ” UIλ₯Ό μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€.
  2. 고유 Dockerλ₯Ό μ§€μ›ν•˜λŠ” 벀더λ₯Ό μ„ νƒν•˜μ‹­μ‹œμ˜€.
  3. 일찍 μ‹€νŒ¨ν•˜κ³  κ°€μž₯ λΉ λ₯Έ ν…ŒμŠ€νŠΈλ₯Ό λ¨Όμ € μ‹€ν–‰ν•˜μ‹­μ‹œμ˜€. λ‹€μ–‘ν•œ λΉ λ₯Έ Inspection(예: 린트, λ‹¨μœ„ν…ŒμŠ€νŠΈ)λ₯Ό κ·Έλ£Ήν™” ν•˜κ³  μ½”λ“œ 컀미터에 λŒ€ν•œ μ‹ μ†ν•œ ν”Όλ“œλ°±μ„ μ œκ³΅ν•  수 μžˆλŠ” 슀λͺ¨ν¬ ν…ŒμŠ€νŠΈ 단계/λ§ˆμΌμŠ€ν†€μ„ λ§Œλ“œμ‹­μ‹œμ˜€.
  4. ν…ŒμŠ€νŠΈ λ³΄κ³ μ„œ, 컀버리지, λ³€ν™”, 둜그 λ“±μ˜ λͺ¨λ“  결과물을 훑어보기 μ‰½κ²Œ ν•˜μ‹­μ‹œμ˜€.
  5. 각 μ΄λ²€νŠΈμ— λŒ€ν•΄ μ—¬λŸ¬ νŒŒμ΄ν”„λΌμΈ/μž‘μ—…μ„ μž‘μ„±ν•˜κ³ , κ·Έ 사이 단계λ₯Ό μž¬μ‚¬μš© ν•˜μ‹­μ‹œμ˜€. 예λ₯Ό λ“€λ©΄, feature 브랜치 μ»€λ°‹μ΄λ‚˜ λ§ˆμŠ€ν„° PR에 λŒ€ν•œ μž‘μ—… ꡬ성. 각 μž¬μ‚¬μš© 둜직이 곡유 단계λ₯Ό μ‚¬μš©ν•˜κ²Œ ν•˜μ‹­μ‹œμ˜€.(λŒ€λΆ€λΆ„μ˜ λ²€λ”λŠ” μ½”λ“œ μž¬μ‚¬μš©μ„ μœ„ν•œ λ©”μ»€λ‹ˆμ¦˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€)
  6. μž‘μ—… 선언에 μ–΄λ–€ν•œκ²ƒλ„ μˆ¨κ²¨λ†“μ§€ λ§ˆμ‹­μ‹œμ˜€.
  7. 릴리슀 λΉŒλ“œμ—μ„œ λͺ…μ‹œμ μœΌλ‘œ 버전을 좩돌 μ‹œμΌœλ³΄κ±°λ‚˜ μ΅œμ†Œν•œ κ°œλ°œμžκ°€ κ·Έλ ‡κ²Œν–ˆλŠ”μ§€ ν™•μΈν•˜μ‹­μ‹œμ˜€.
  8. ν•œλ²ˆλ§Œ λΉŒλ“œν•˜κ³  단일 λΉŒλ“œ κ²°κ³Όλ¬Ό(예: Docker 이미지)에 λŒ€ν•΄ λͺ¨λ“  검사λ₯Ό μˆ˜ν–‰ν•˜μ‹­μ‹œμ˜€.
  9. λΉŒλ“œκ°„μ— μƒνƒœκ°€ λ³€ν•˜μ§€ μ•ŠλŠ” μž„μ‹œ ν™˜κ²½μ—μ„œ ν…ŒμŠ€νŠΈν•˜μ‹­μ‹œμ˜€. node_modules 캐싱은 μœ μΌν•œ μ˜ˆμ™Έ 일 수 μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: μˆ˜λ…„κ°„μ˜ λ…Έν•˜μš°λ₯Ό λ†“μΉ˜λŠ” 것과 κ°™μŠ΅λ‹ˆλ‹€.

βšͺ ️ 5.9 λΉŒλ“œ 맀트릭슀: μ—¬λŸ¬ λ…Έλ“œ 버전을 μ‚¬μš©ν•΄μ„œ λ™μΌν•œ CI 단계λ₯Ό μ‹€ν–‰ ν•˜μ‹­μ‹œμ˜€.

βœ… μ΄λ ‡κ²Œ 해라: ν’ˆμ§ˆ κ²€μ‚¬λŠ” μ„ΈλŸ°λ””ν”Όν‹°μ— κ΄€ν•œ κ²ƒμœΌλ‘œ, 문제λ₯Ό 쑰기에 λ°œκ²¬ν•˜λŠ”λ° 도움이 λ˜λŠ” 더 λ§Žμ€ 기회λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. μž¬μ‚¬μš© κ°€λŠ₯ν•œ νŒ¨ν‚€μ§€λ₯Ό κ°œλ°œν•˜κ±°λ‚˜ λ‹€μ–‘ν•œ ꡬ성 및 λ…Έλ“œ λ²„μ „μœΌλ‘œ μ—¬λŸ¬ 고객의 μ œν’ˆμ„ μ‹€ν–‰ν•˜λŠ” 경우, CIλŠ” λͺ¨λ“  κ΅¬μ„±μ˜ μˆœμ—΄μ— λŒ€ν•΄ ν…ŒμŠ€νŠΈ νŒŒμ΄ν”„ 라인을 μ‹€ν–‰ν•΄μ•Όν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 일뢀 고객은 MySQL을 μ‚¬μš©ν•˜κ³  λ‹€λ₯Έ 고객은 PostgreSQL을 μ‚¬μš©ν•œλ‹€κ³  κ°€μ •ν•©μ‹œλ‹€. 일뢀 CI λ²€λ”λŠ” '맀트릭슀'λΌλŠ” κΈ°λŠ₯을 μ œκ³΅ν•˜μ—¬ MySQL, PostgreSQL 및 8, 9, 10κ³Ό 같은 μ—¬λŸ¬ λ…Έλ“œ λ²„μ „μ˜ λͺ¨λ“  μˆœμ—΄μ— λŒ€ν•΄ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 κ²½μš°μ—λŠ” μ–΄λ– ν•œ μΆ”κ°€ λ…Έλ ₯없이 ꡬ성(μ„€μ •)λ§Œμ„ μ‚¬μš©ν•˜μ—¬ κ°€λŠ₯ν•©λ‹ˆλ‹€(ν…ŒμŠ€νŠΈ λ˜λŠ” 기타 ν’ˆμ§ˆ 검사가 μžˆλ‹€κ³  κ°€μ •). 맀트릭슀λ₯Ό μ§€μ›ν•˜μ§€ μ•ŠλŠ” λ‹€λ₯Έ CIλŠ” ν™•μž₯μ΄λ‚˜ 쑰정이 ν•„μš”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

❌ 그렇지 μ•ŠμœΌλ©΄: So after doing all that hard work of writing testing are we going to let bugs sneak in only because of configuration issues?

✏ 예제 μ½”λ“œ

πŸ‘ μ˜¬λ°”λ₯Έ 예: Travis(CI 벀더) λΉŒλ“œ μ •μ˜λ₯Ό μ‚¬μš©ν•˜μ—¬ μ—¬λŸ¬ λ…Έλ“œ 버전에 λŒ€ν•œ λ™μΌν•œ ν…ŒμŠ€νŠΈλ₯Ό μ‹€ν–‰ν•˜μ‹­μ‹œμ˜€.

language: node_js
- "7"
- "6"
- "5"
- "4"
- npm install
- npm run test


Yoni Goldberg

Role: μ €μž

About: μ €λŠ” 포좘 500λŒ€ κΈ°μ—… 및 μŠ€νƒ€νŠΈμ—…κ³Ό ν•¨κ»˜ JS 및 Node.js μ–΄ν”Œλ¦¬μΌ€μ΄μ…˜μ„ κ°œλ°œν•˜λŠ” 독립 μ»¨μ„€ν„΄νŠΈμž…λ‹ˆλ‹€. λ‹€λ₯Έ μ–΄λ–€ μ£Όμ œλ³΄λ‹€ 더 ν₯λ―Έλ₯Ό λ„λŠ” ν…ŒμŠ€νŠΈ κΈ°μˆ μ„ μŠ΅λ“ν•˜λŠ” 것을 λͺ©ν‘œλ‘œ ν•©λ‹ˆλ‹€. λ˜ν•œ Node.js Best Practices의 μ €μžμ΄κΈ°λ„ ν•©λ‹ˆλ‹€.

Workshop: πŸ‘¨β€πŸ« μ΄λŸ¬ν•œ λͺ¨λ“  ν”„λž™ν‹°μŠ€μ™€ κΈ°μˆ μ„ 배우고 μ‹ΆμŠ΅λ‹ˆκΉŒ?(유럽 & λ―Έκ΅­) ν…ŒμŠ€νŠΈ μ›Œν¬μƒ΅μ— λ“±λ‘ν•˜μ‹­μ‹œμ˜€


Bruno Scheufler

Role: 기술 κ²€ν†  및 κ³ λ¬Έ

λͺ¨λ“  ν…μŠ€νŠΈλ₯Ό μˆ˜μ •, κ°œμ„ , lint 및 λ‹€λ“¬μ—ˆμŠ΅λ‹ˆλ‹€.

About: ν’€ μŠ€νƒ μ›Ή μ—”μ§€λ‹ˆμ–΄, Node.js 및 GraphQL의 μ—΄λ ¬ν•œ μ§€μ§€μž

Ido Richter

Role: 컨셉, λ””μžμΈ 및 ν›Œλ₯­ν•œ μ‘°μ–Έ

About: μ •ν†΅ν•œ ν”„λ‘ νŠΈ μ—”λ“œ 개발자, CSS μ „λ¬Έκ°€ 및 이λͺ¨ν‹°μ½˜μ— 관심이 λ§Žμ€ μ‚¬λžŒ

You can’t perform that action at this time.