※ 이 글은 How to Use Extend with TypeSript 를 번역한 글입니다.

 

출처 - 구글 이미지 검색

mkdir src && cd src
mkdir __tests__
mkdir typings
mkdir example

 

Jest 는 현재 가장 인기 있는 테스팅 라이브러리 중 하나 입니다. 코드의 안정성과 신뢰성을 보장해주기 위해서 테스트 코드의 작성 여부는 선택이 아닌 필수가 되어버렸습니다. 

 

 

 

최근 1년간 NPM 다운로드 현황 (출처 - NPM 트렌드 현황)

 

 

기본적으로 Jest 는 여러 환경에서 테스트 코드를 작성할 수 있는 환경을 제공합니다. 물론 이 환경에서는 비동기 호출이나 다소 복잡했던 로직들도 테스트할 수 있는 환경 역시 구축할 수 있습니다. 그런데 Jest 가 제공하는 환경에서가 아니라 좀 더 구체적으로 내가 작업하고 있는 환경에 맞게 테스트하고 싶은 경우는 어떻게 해야할까요. 저의 경우는 "서버로 데이터를 요청하는 API 요청에 대한 응답값이 JSON 스키마와 항상 일치하는지 어떻게 확신할 수 있을까?" 에 대한 고민이 있었습니다. 왜냐하면 실제 프로덕션 레벨에서 다뤄지는 서버 데이터는 매 요청마다 결과 값이 다를 수 있기 때문에 상황이 복잡합니다. 주로 실시간 데이터인 항공 데이터나 숙박 검색과 같은 경우가 이런 경우입니다. 

 

 

이럴 때 Jestextend 는 커스터마이징 테스트 메소드를 구현할 수 있게 해줍니다.

 

 

 

 

프로젝트 구성하기

작업을 하기에 앞서 먼저 프로젝트를 구성을 시작하겠습니다.

npm init -y

 

 

필요한 패키지 역시 설치해 줍니다.

npm i -D jest ts-jest typescript @types/jest

// package.json
"scripts": {
  "test": "jest"
}

 

 

 

마지막으로, 폴더를 생성하면 끝입니다.

mkdir src && cd src
mkdir __tests__
mkdir typings
mkdir example

 

여기까지 따라왔다면, 디렉토리 구조는 아래와 같이 생성 됐을 것입니다. 간단해 보이지만 (실제로 간단하지만) 여기까지 잘 따라오셨다면 이제 모든 준비가 된 겁니다!

node_modules
src
|--- __tests__
|--- example
|--- typings
package-lock.json
package.json

 

 

 

 

 

설정 파일 작성하기

이 단계에서는, 타입스크립트가 우리가 테스트 코드를 작성할 때, 새롭게 만든 타입 함수들을 사용할 때 그게 뭔지 이해할 수 있도록 하는 사전 작업이 필요합니다. 별 다른 어려움이 없으니 천천히 따라해보세요.

 

 

먼저, 루트폴더에 jest.config.js 파일과 tsconfig.json 파일을 만드시기 바랍니다. 이 파일들은 테스트 코드를 실행할 때 파일 런타임 때 자동으로 읽히게 될 것입니다. 설정 파일을 따로 만들고 싶지 않다면 package.json 파일에 설정을 추가하는 식으로 대신할 수도 있습니다. 좀 더 많은 정보가 필요하다면 Jest 환경 설정을 클릭하세요.

 

touch jest.config.js
cd src && touch tsconfig.json

 

 

 

 

 

 

 

 

 

 

 

 

 

기본 테스트코드 작성

이제 기본적인 테스트 코드를 작성해 봅시다.

 

cd src && cd example
touch math.ts

 

 

 

 

 

그리고 방금 생성한 math.ts 파일을 테스트할 테스트 파일을 만듭니다.

테스트 파일을 만들 때 주의할 점은 반드시 테스트 파일 이름은 *.spec.* 형식으로 지어져야 합니다.

 

cd __tests__
touch math.spec.ts

 

 

 

 

 

mockedAdd 는 spy 함수로 spy 함수내에 파라미터로 전달된 원래 함수(math.add)가 사용됐을 때 추적을 할 수 있도록 하는 아주 유용한 기능의 메소드입니다. 원래 함수가 만약 객체라면 spy 함수는 그 객체의 주소 값을 복사한 값을 갖고 있는 새 변수라고 생각하는 것이 더 쉬울 수도 있겠습니다.

 

const initialObj = { sayHi: () => console.log('hi') };

// initialObj 의 주소 값을 가집니다.
// initialObj 의 변경점에 대해서 실시간으로 같이 공유받을 수 있다는 점을 기억하세요.
const spyObj = initialObj;

// spyObj 와 비슷한 개념으로(그러나 엄연히 다른 개념입니다) 기존 객체의 메소드의 변경 사항을 
// 계속 실시간으로 추적할 수 있다고 생각해보세요.
// 어떤 파라미터로 불렸는지, 몇 번 실행 됐는지 등 다양하게 사용처를 추적할 수 있습니다.
const spyFn = jest.spyOn(initialObj, 'sayHi');

spy 함수에 대해 더 자세한 정보를 보고 싶으시면 Jest 공식 문서를 참조하세요.

 

 

테스트 코드를 보면 expect() 를 통해 테스트하는 부분에서 서로 다른 방식으로 테스트를 하고 있는 부분을 보실 수 있을 겁니다. 위에서 짧게나마 소개한 spy 함수와 원래 함수의 반환 값을 이용해 테스트를 진행할 수도 있고 mock 객체에 포함된 calls, results 같은 원본 함수 추적용 배열을 사용해 테스트를 할 수도 있습니다.

 

 

그런데 테스트 코드를 작성하다보면 기본 테스트 메소드를 사용할 수 없는 경우가 있습니다. 예를 들어 값을 전달 받아 분기 처리를 한 후에 결정해야하는 경우 등이 그렇습니다. 그럴 때마다 그럼 테스트 코드를 이런식으로 작성해야 할까요?

 

if (num % 2 === 1) {
  expect(num).toBe(yourOdd);
} else {
  expect(num).not.toBe(yourOdd);
}

 

 

expect() 를 사용해 저런식으로 가정법과 같이 값에 따라 결과가 불확실한 형태로 테스트 코드를 작성하는 건 권장하고 싶지 않습니다. 테스트 코드 블록의 느낌을 산만하게 만들고 한 눈에 들어오기 점점 더 어려워지기 때문입니다.

 

 

이럴 때 사용하는 기능이 있습니다.

 

 

expect.extend()

Jest 는 테스터들이 좀 더 자유롭게 테스트 함수(이제부터는 matchers 라고 부르겠습니다) 를 입맛에 맞게 작성해서 사용할 수 있는 자율성을 갖고 있습니다. 타입스크립트에서 Jest 를 사용하는 경우, 타이핑 파일에 커스텀 matchers 에 대한 타이핑 정보가 없기 때문에 우리는 타입스크립트가 이해할 수 있도록 타이핑 파일을 먼저 작성하는 것부터 시작하겠습니다.

 

 

expect() 로 넘기는 값이 홀수인지 판별하는 커스텀 matcher 를 만들겠습니다.

 

 

cd src && cd typings
touch index.d.ts

 

 

 

 

 

 

여기까지 따라왔을 경우 폴더 구조는 다음과 같아야합니다.

 

node_modules
src
|--- __tests__
|    |
|    |--- math.spec.ts
|--- example
|    |
|    |--- math.ts
|--- typings
|    |
|    |--- index.d.ts
|
|--- tsconfig.json
jest.config.js
package-lock.json
package.json

 

typings 폴더를 만들고 그 안의 타이핑 파일에 toBeOdd() 에 대한 타입을 정의해주었지만 아직은 테스트 코드에 그 어떤 영향도 미치고 있지 않습니다. 타이핑 파일은 단순히 타입스크립트가 toBeOdd() 라는 메소드가 존재하고 어떤 파라미터를 받고 어떤 형태의 값을 반환해야하는지 알 수 있게해주는 파일입니다. 더 쉽게 말하자면 예를 들어서 window. 같이 window 를 입력하고 . 를 입력했을 때 내부에 포함된 메소드 목록들 중 하나로 나올 수 있게 타입스크립트한테 정보를 전달해주는 파일입니다. 

 

 

이제 테스트 코드로 돌아가서 실제 사용법을 보겠습니다.

 

 

 

 

 

 

 

 

 

 

 

우리가 만든 새로운 matchertoBeOddreceived 라는 파라미터를 제외하고 아무 파라미터도 받지 않는 함수입니다. received 는 테스트 코드에서 expect() 를 호출했을 때 그 안에 넘겨주는 값입니다. 즉 expect(10) 으로 시작했다면 received 의 값은 10 입니다. 위 코드에서는 received 는 각각 2 와 3 입니다. 

 

 

toBeOdd 메소드에 파라미터를 추가로 넘기고 싶다면 타이핑 파일부터 다시 수정해야하는데, 관련된 변경 사항들만 짚어보자면 다음과 같이 될 겁니다.

 

// index.d.ts
declare global {
  ...
  interface matchers<R> { 
    toBeOdd(a: number): R;
  }
}
// math.spec.ts
expect.extend({
  toBeOdd(received, a) {
    .. do something
  }
});
it('..', () => {
  /* received will be 3, and a will be 2 */
  expect(3).toBeOdd(2);
})

 

 

matchermessagepass 라는 값을 포함한 객체를 반드시 반환 값으로 반환해야 합니다.

 

 

message - 테스트가 실패 했을 경우 콘솔에 보여줄 오류 메시지를 반환하는 메소드입니다. 아무 인자도 받지 않습니다.

pass - boolean 타입의 값으로 어떤 형식으로 테스트가 불렸는지에 따라 각각의 message 함수를 리턴할 수 있게 해줍니다.

 

 

예를 들어서 pass 가 true 인 경우, message 함수는 matcherexpect(value).not.toBeOdd() 같은 형식으로 불려진 상황에서 테스트가 실패했을 때 실행됩니다. 반대로 pass 가 false 인 경우는 matcherexpect(value).toBeOdd() 의 형식으로 테스트가 실행된 상태에서 테스트가 실패했을 때 실행되게 됩니다.

 

 

/*
* pass: false
* 2 는 홀수가 아니므로 테스트는 실패합니다
* message 함수가 반환하는 메시지는 
* 'expected 2 not to be an odd number'
*/
expect(2).toBeOdd();
/*
* pass: true
* 1 은 홀수이므로 테스트는 실패합니다
* message 함수가 반환하는 메시지는 
* 'expected 1 to be an odd number'
*/
expect(1).not.toBeOdd();

 

 

 

최종 코드

 

npm run test math

 

 

 

 

 

결론

자바스크립트 테스팅 라이브러리는 프로젝트가 배포되기 전 코드의 품질을 보증할 수 있게 해줍니다. 그래도 테스트 코드를 작성하다보면 어느 테스팅 라이브러리를 사용하고 있든 상황에 맞는 필요한 matcher 를 찾기 힘든 상황이 오기 마련입니다. 그래서 이번 포스팅에서 소개해드린 기능은 여러분이 작성하고 있는 테스트 코드의 유연함을 더욱 증가시켜줄 것입니다.

제 블로그에 관심 있으시고 JavaScript를 공부해야겠다고 생각되시는 분들은 읽어보시는 것도 좋을 듯 합니다.


JavaScript를 제대로 이해하지 못하고 있는 분들을 대상으로, 기초 내용부터 약간의 심화까지 담아 보려고 합니다.


1. JavaScript 기초

- 변수 Scope, 호이스팅, JavaScript에서의 함수란, 프리미티브 값

- this

- 클로져(closure)


2. Prototype

- 프로토타입의 기초 이해

- 프로토타입 관계도 실습

- 프로토타입을 이용한 상속


3. 성능 향상 / 최적화

- for loops

- DOM

- Event handlers

- 기타


4. 그 외의 이야기

- ES6

- Framework / Libraries



혹시나 질문이 개인적으로 있으신분들은 방명록에 비밀글로 남겨주시면 답변드릴 수 있는 범위 내에선 답변 드리겠습니다~





※ 해당 포스트는 Dave Ceddia 님의 동의 하에 글을 번역한 것임을 알립니다.


※ 자연스러운 글의 흐름을 위해 작성자의 임의로 의역한 부분이 있음을 알립니다.







React 컴포넌트가 제대로 렌더링이 안되고 있습니까?


그 전에 잠깐! React 컴포넌트가 componentWillMount 에서 서버로부터 데이터를 받아오는 작업을 아래와 같이 하고 있다면, 과연 무엇이 렌더링이 될까요?



사진의 출처 - Jay Galvin



만약 '아무것도 일어나지 않는다' 혹은 'a console error'라고 생각하셨다면, 정답입니다.

만약 '데이터를 받아온다'고 생각했다면, 이 포스트를 읽으시는게 좋겠군요.


State는 초기화되어 있지 않다

우선 여기서 짚고 넘어가야할 두 가지 중요한 사실들이 있습니다:

1. 컴포넌트의 상태 값(e.g. this.state) 의 시작 값은 null이다.
2. 비동기적으로 데이터를 처리할 때, 컴포넌트는 적어도 한 번, 데이터가 불러지기 전에 렌더링 된다.

- 데이터가 constructor, componentWillMount , componentDidMount 혹은 그 어디에 있는지와 상관이 없이 말이다.


그렇습니다. constructorcomponentWillMount가 최초의 렌더링 그 이전에 실행이 된다고 할지라도, 비동기 호출은 컴포넌트가 비동기 이전에 렌더링이 되는 것을 막아주지는 못합니다. 그래서 여러분이 여전히 문제를 일으키고 있는 것이고요.


해결방안

사실 해결책은 간단합니다. 가장 쉬운 방법은 constructor 안에서 state를 그럴싸한 값으로 기본 설정해주는 것입니다.


위의 컴포넌트에 대한 예제는 아래와 같이 풀어볼 수 있습니다.


그리고 아래처럼 또한 빈 값을 render 안에서 설정해줄 수도 있습니다.



이 것은 이상적인 방법은 아닙니다. 만약 기본 설정 값(default value)을 부여할 수 있다면, 그렇게 하세요.


낙수효과(Trickle-Down effect)의 실패에 따른 부작용

"비어있는 상태값(state)"이 만약 제대로 설정이 되어 있지 않다면, 결국에 이 문제는 개발자에게 더 큰 후폭풍을 안겨주게 됩니다.

(※ 작성자에 의한 의역임을 알림(or를 오타로 판단해 of로 해석). 원문 : The lack of default or “empty state” data can bite you another way, too)

바로 자식 컴포넌트에게 정의되지 않은(undefined) 상태값이 전달될 때 말이죠.


위의 예제를 조금 더 확장해 봅시다. 가령 Quiz 컴포넌트안에 있는 자식 컴포넌트로 list를 넘겨준다고 한다면,



문제가 보이시나요? Quiz가 처음으로 렌더링이 될 때, this.state.itemsundefined 입니다. 무슨 말인가하면, ItemList 컴포넌트가 items 이라는 데이터를 undefined 상태로 넘겨받게되고, 결국에는 화면에서 Uncaught TypeError: Cannot read property 'map' of undefined 를 보게되는 것이란 말입니다.


만약 ItemList안에 propTypes에 대한 정의가 컴포넌트에 새겨져 있었다면 디버깅하기는 더 수월했겠군요.



이렇게 정의가 되어 있었다면 콘솔에 나오는 메시지는 다음과 같습니다.

"Warning: Failed prop type: Required prop items was not specified in ItemList."

(경고: prop type 오류: 필수요건인 itemsItemList에 정의되어 있지 않음.)


그러나 여전히 Uncaught TypeError: Cannot read property 'map' of undefined 의 에러는 남아있습니다. propType의 유효성을 검사하는 것은 컴포넌트의 렌더링에 관한 문제를 해결해주지는 않습니다. 다만 알려주기만 할 뿐이죠.


그러나 최소한 이러한 방법은 디버그를 조금 더 수월하게는 해줍니다.


기본 Props 설정(Default Props)

한 가지 더 쉬운 방법이 있습니다. props의 default값을 설정해주면 됩니다.


Default props는  항상 최선책은 아닙니다. 이 방법을 사용하기전에 더 나은 해결책이 없는지, 이 방법이 그저 임기응변(band-aid solution)에 지나지 않는지 항상 확인하세요.


Default 값들을 설정하는 것이 그저 값들이 정의되지도 않아서 발생하는 수 많은 에러들을 모두 예방해줄 수 있을 것이라 생각하시나요? 그 전에 값(data)들을 제대로 정의하는 것을 먼저 고려하십시오.


아니면 prop이 단순한 선택적인 것에 불과하다고 말할 수 있습니까? prop이 전달되지 않은 상태에서 컴포넌트가 렌더링이 되는 경우에 대해서는 문제가 없다고 생각하십니까? 그렇다면 기본 값 설정에 대한 문제 해결을 받아들일 준비가 되었습니다.


이 문제는 몇 가지 방법으로 해결할 수 있습니다.


defaultProps 이용하기

이 메소드는 여러분의 컴포넌트가 상태(state)를 갖고 있지 않은 함수인지, React.Component를 상속받는 클래스인지에 얽매이지 않습니다.



이 메소드는 컴포넌트가 클래시 형식일 때, 여러분의 컴파일러가 ES7의 static initializer 문법을 지원할때만 동작합니다. 



render 조작

이 방법은 아래에 있는 render 안에서, ES6의 destructuring syntax에 의해 다음과 같이 표현됩니다.



이 라인은 다음과 같은 의미입니다.

"items 키를 this.props에서 가져오고, 만약 undefined상태라면 빈 배열로 초기화한다"



인자 값(arguments) 조작

만약 컴포넌트가 함수형이라면, 아래와 같이 초기화를 해줄 수 있습니다.



요약

  • 컴포넌트 라이프 사이클이 실행되는 동안에 이루어지는 비동기 호출은 컴포넌트가 데이터가 로드되기 전에 렌더링이 된다는 것을 의미합니다. 그래서..

  • constructor 안에서 state를 초기화하거나 빈 값을 적절히 처리해야 합니다.

  • 디버깅을 더 수월히 하기 위해 ProTypes를 사용하십시오.

  • defaulProps를 적절히 활용하세요.

  • 구조 변경(※ 의역, 원문: Destructuring syntax: 구문 파괴)은 깔끔하면서 쉽게 기본 값들을 설정할 수 있습니다.




React.js를 사용해 개발을 하다보면 사실상 눈에 띌 정도로 '좋다'고 느끼는 것은 그다지 많지 않습니다. React에익숙하지 않은 개발자라면 이러한 현상이 더욱 드러날 것이겠지요. 그런데도 우리가 React를 사용하는 이유는 바로 이런것이 아닐까 싶네요.


React의 공식 홈페이지의 내용을 조금만 인용해 등장배경부터 알아 보자면 다음과 같습니다.

  • By unifying your markup with its corresponding view logic, React can actually make views easier to extend and maintain.
  • 여러분이 만들어놓은 마크업(markup)과 그에 상응하는 뷰 로직을 하나로 만들 때, React는 실제로 뷰를 더욱 확장성 있고 관리하기 쉽도록 만들어줍니다.

출처 - https://facebook.github.io/react/blog/2013/06/05/why-react.html


실제로 JavaScript나 jQuery만을 이용해 어플리케이션을 개발했을 때를 생각을 해보자면, 하나의 HTML파일에 여러가지 모듈화된 js파일들을 불러왔었을 겁니다. 어플리케이션의 크기가 증가하면서 코드의 양도 자연스레 비례적으로 증가하였고 단편적으로 HTML의 양이 대폭적으로 늘어났다고 가정을 해보면, 우선 분리되어있지 않은 코드들이 가독성을 제일 먼저 떨어트릴 것입니다. 그리고 이러한 DOM들을 제어 · 관리하는데에 사용하는 jQuery의 양도 매우 많아지겠죠. 즉, 한마디로 말해 '관리의 효율' 문제라는 겁니다. 아무리 버그가 없고 이상이 없는 사이트인데 오늘만 산다는 마음가짐으로 코드 관리를 매우 비효율적으로 해놓는다면, 추후의 관리의 차원에서 너무 힘들어지겠죠. 나 혼자만 개발하는 것이 아니라 협업의 관계에 속해있다면 관리는 반드시 고려해야할 숙제 중 하나입니다.

다행스럽게도, React는 이러한 관리문제를 컴포넌트의 모듈화로 제법 해결해놓았습니다.  아래의 코드는 아주 간단한 jQuery 예제로 jQuery가 어떻게 DOM에게 간섭하는지를 보여줍니다. jQuery의 시작은 항상 $으로 시작하죠, 이 기호를 이용해 모든 DOM에 접근할 수 있습니다. (솔직히 제 개인적으로도 jQuery만큼 DOM관리하는데에 편한것도 없는 것 같습니다.)

See the Pen PmBLLZ by Munkyu Yang (@moonformeli) on CodePen.


아래에는 같은 내용의 코드를 React.js를 통해서 구현해본 것입니다.

See the Pen WjKmqj by Munkyu Yang (@moonformeli) on CodePen.

언뜻 보면 React.js의 코드가 더 길고 복잡해 보입니다. HTML 코드는 jQuery의 코드를 기준으로 8줄이네요. 그리고 React는 코드를 컴포넌트라는 하나의 클래스를 만들어 모듈화를 통해 관리합니다. 즉, '뚜렷한 의미를 가지는 클래스들의 모듈화' 를 이용하는 것이죠. 이는 상당히 중요한 개념입니다. 제 개인적으로는 React가 jQuery보다 앞서있다고 생각되는 점 중에 하나입니다.


만약에, 어플리케이션이 방대해져서 HTML만 1,000줄이 필요하다고 가정을 해보면, 무수히 많은 클래스들과 아이디가 부여가 될 것이고 그에 따른 CSS제어, 액션제어들이 필요하겠죠. 이 모든 것들을 $.ready()에서 처리한다고 생각해본다면 얼마나 읽기가 힘들지는 눈에 선하죠? jQuery에서도 js파일을 나눠서 관리할 수 있습니다만.. 특정한 의미가 없이 그저 코드의 양을 줄이고자 나누는 작업은 오히려 가독성을 낮출 수 있습니다. 하나의 파일에서 시간을 조금만 들여서 보면 될 것을 파일의 분기때문에 이 파일 저 파일 번갈아가면서 봐야할 수도 있는거죠.


React는 1,000줄의 HTML에서 이런식으로 흐름을 분기합니다. 


이런식의 의미있는 컴포넌트만 불러와 하나의 HTML을 만드는 것은 상당히 편리하고 강력합니다. 또 <로그인 />에서는 로그인에 필요한 액션 제어, <회원가입/> 에서는 그에 맞는 액션제어(Form 전송 등)를 알맞게 할 수 있고, 나중을 위한 유지보수의 첫 단추도 잘 넣게 되는 것이죠.



그러나, 한 가지 염두해두고 있어야 할 점은, React가 낫다, jQuery가 낫다 아니면 혹은 다른 프레임워크들이 낫다고 생각해 한 가지만을 고집하는 것은 어리석은 생각입니다. 공식 홈페이지에서도 나와있듯이, React는 MVC 패턴을 구현하기 위해 제작된 프레임워크가 아닙니다. 그저 확장성과 관리효율성 차원에서 고려되어져 만들어진 우수한 성능의 자바스크립트 라이브러리일 뿐입니다. 기본적인 베이스를 잡고 있는 프레임워크나 라이브러리는 존재하되, 필요한 기능을 더 편리하고 강력하게 구현하는 기능을 제공해주는 것이 있다면, 현재 사용중인 개발언어의 기능과 비교해보고 더 나은 것을 선택하는 것이 바람직하다고 할 수 있습니다.


+ Recent posts