@@ -176,10 +176,223 @@ console.log(dom.window.document.querySelector('p').textContent) // "Hello World
176
176
3 . 함수나 모듈의 실제 반환 값을 적는다
177
177
4 . 3번의 기대에 따라 2번의 결과가 일치하는지 확인한다
178
178
5 . 기대하는 결과를 반환한다면 테스트는 성공이며, 만약 기대와 다른 결과를 반환하면 에러를 던진다
179
+ - Node.js 기본 제공 assert 활용
180
+ ```
181
+ const assert = require('assert')
182
+
183
+ function sum(a, b) {
184
+ return a + b
185
+ }
186
+
187
+ assert.equal(sum(1, 2), 3)
188
+ assert.equal(sum(2, 2), 3) // AssertionError
189
+ ```
190
+ 좋은 테스트 코드는 다양한 테스트 코드가 작성되고 통과하는 것뿐만 아니라 어떤 테스트가 무엇을 테스트하는지 일목요연하게 보여주는 것도 중요
191
+ - Jest 테스팅 프레임워크 사용
192
+ ```
193
+ function sum(a, b) {
194
+ return a + b
195
+ }
196
+
197
+ module.exports = {
198
+ sum,
199
+ }
200
+
201
+ // math.test.js
202
+ const { sum } = require('./math');
203
+
204
+ test('두 인수가 덧셈이 되어야 한다.', () => {
205
+ expect(sum(1, 2)).toBe(3)
206
+ })
207
+
208
+ test('두 인수가 덧셈이 되어야 한다.', () => {
209
+ expect(sum(2, 2).toBe(3)
210
+ }) // 에러
211
+ ```
179
212
### 8.2.3 리액트 컴포넌트 테스트 코드 작성하기
213
+ 1 . 컴포넌트를 렌더링한다
214
+ 2 . 필요하다면 컴포넌트에서 특정 액션을 수행한다
215
+ 3 . 컴포넌트 렌더링과 2번의 액션을 통해 기대하는 결과와 실제 결과를 비교한다
216
+ ```
217
+ import Reac from 'react';
218
+ import { render, screen } from '@testing-library/react';
219
+ import App from './App';
220
+
221
+ test('renders learn react link', () => {
222
+ render(<App/>);
223
+ const linkElement = screen.getByTest(/learn react/i);
224
+ expect(linkElement).toBeInTheDocument();
225
+ })
226
+ ```
227
+ - getBy..
228
+ 인수의 조건에 맞는 요소를 반환
229
+
230
+ 해당 요소가 없거나 두 개 이상이면 에러를 발생
231
+
232
+ 복수 개를 찾을 경우 getAllBy...를 사용
233
+
234
+ - findBy...
235
+ getBy...와 유사하나 Promise를 반환
236
+
237
+ 비동기로 찾으며 1000ms의 타임아웃 기본값
238
+
239
+ 두 개 이상이면 에러를 발생시키며 findAllBy...를 활용
240
+
241
+ 비동기 액션 이후에 요소를 찾을 때 사용
242
+ - queryBy...
243
+ 인수의 조건에 맞는 요소를 반환하는 대신, 찾지 못한다면 에러가 아닌 null을 반환
244
+
245
+ 복수일 경우 queryAllBy... 활용
246
+ #### 정적 컴포넌트
247
+ ```
248
+ import { render, screen } from '@testing-library/react';
249
+
250
+ import StaticComponent from './index';
251
+
252
+ beforeEach(() => { // 각 테스트(it)를 수행하기 전에 실행하는 함수
253
+ render(<StaticComponent />)
254
+ })
255
+
256
+ describe('링크 확인', () => { // 비슷한 속성을 가진 테스트를 하나의 그룹으로 묶는 역할. describe in describe 가능
257
+ it('링크가 3개 존재한다.', () => {
258
+ const ul = screen.getByTestId('ul');
259
+ expect(ul.children.length).toBe(3)
260
+ })
261
+
262
+ it('링크 목록의 스타일이 square다.', () => { // it은 test의 alias
263
+ const ul = screen.getByTestId('ul');
264
+ expect(ul).toHaveStyle('list-style-type: square;')
265
+ })
266
+ })
267
+
268
+ describe('리액트 링크 테스트', () => {
269
+ it('리액트 링크가 존재한다.', () => {
270
+ const reactLink = screen.getByText('리액트');
271
+ expect(reactLink).toBeVisible();
272
+ })
273
+
274
+ it('리액트 링크가 올바른 주소로 존재한다', () => {
275
+ const reactLink = screen.getByText('리액트');
276
+ expect(reactLink.tagName).toEqual('A');
277
+ expect(reactLink).toHaveAttribute('href', 'https://reactjs.org')
278
+ })
279
+ })
280
+
281
+ describe('네이버 링크 테스트', () => {
282
+ it('네이버 링크가 존재한다.', () => {
283
+ const naverLink = screen.getByText('네이버');
284
+ expect(naverLink).toBeVisible();
285
+ })
286
+
287
+ it('네이버 링크가 올바른 주소로 존재한다', () => {
288
+ const naverLink = screen.getByText('네이버');
289
+ expect(naverLink.tagName).toEqual('A');
290
+ expect(naverLink).toHaveAttribute('href', 'https://naver.com')
291
+ })
292
+
293
+ it('네이버가 같은 창에서 열려야 한다.', () => {
294
+ const naverLink = scree.getByText('네이버');
295
+ expect(naverLink).not.toHaveAttribute('target');
296
+ })
297
+ })
298
+ ```
299
+ #### 동적 컴포넌트
300
+ - 사용자가 useState를 통해 입력을 변경하는 컴포넌트
301
+ ```
302
+ import { fireEvent, render } from '@testing-library/react';
303
+ import useEvent from '@testing-library/user-event';
304
+
305
+ import { InputComponent } from '.';
306
+
307
+ describe('InputComponent 테스트', () => {
308
+ const setup = () => { // 현재 파일의 모든 테스트가 렌더링과 button, input을 필요로 하므로 하나의 함수로 묶어둠
309
+ const screen = render(<InputComponent/>);
310
+ const input = screen.getByLabelText('input') as HTMLInputElement
311
+ const button = screen.getByText(/제출하기/i) as HTMLButtonElement
312
+ return {
313
+ input,
314
+ button,
315
+ ...screen,
316
+ }
317
+ }
318
+
319
+ it('input의 초깃값은 빈 문자열이다', () => {
320
+ const { input } = setup()
321
+ expect(input.value).toEqual('')
322
+ })
323
+
324
+ it('input의 최대 길이가 20자로 설정돼 있다', () => {
325
+ const { input } = setup()
326
+ expect(input).toHaveAttribute('maxlength', '20')
327
+ })
328
+
329
+ it('영문과 숫자만 입력된다.', () => {
330
+ const { input } = setup()
331
+ const inputBalue = '안녕하세요123'
332
+ userEvent.type(input, inputValue) // 사용자가 타이핑하는 것을 흉내내는 메서드
333
+ expect(input.value).toEqual('123')
334
+ })
335
+
336
+ it('아이디를 입력하지 않으면 버튼이 활성화되지 않는다', () => {
337
+ const { button } = setup()
338
+ expect(button).toBeDisbaled()
339
+ })
340
+
341
+ it('아이디를 입력하면 버튼이 활성화된다', () => {
342
+ const { button, input } = setup()
343
+
344
+ const inputValue = 'helloWorld'
345
+ userEvent.type(input, inputValue)
346
+
347
+ expect(input.value).toEqual(inputValue)
348
+ expect(button).toBeEnabled()
349
+ })
350
+
351
+ it('버튼을 클릭하면 alert가 해당 아이디로 표시된다', () => {
352
+ const alertMock = jest
353
+ .spyOn(window, 'alert') // 어떠한 특정 메서드를 오염시키지 않고 실행이 됐는지, 어떤 인수로 실행됐는지 등 실행과 관련된 정보만 얻고 싶을 때 사용, alert를 구현하지 않고 해당 메서드가 실행됐는지만 관찰
354
+ .mockImplementation((_: string) => undefined) // Node.js 환경에는 window.alert가 존재하지 않으므로 해당 함수를 모의 함수로 구현
355
+
356
+ const { button, input } = setup()
357
+ const inputValue = 'helloworld'
358
+
359
+ userEvent.type(input, inputValue)
360
+ fireEvent.click(button)
361
+
362
+ expect(alertMock).toHaveBeenCalledTimes(1)
363
+ expect(alertMock).toHaveBeenCalledWith(inputValue)
364
+ })
365
+ })
366
+ ```
367
+ - 비동기 이벤트가 발생하는 컴포넌트
368
+ ```
369
+ jest.spyOn(window, 'fetch').mockImplementation(
370
+ jest.fn(() =>
371
+ Promise.resolve({
372
+ ok: true,
373
+ status: 200, // 모든 서버 오류 케이스 작성 필요
374
+ json: () => Promoise.resolve(MOCK_TODO_RESPONSE),
375
+ }),
376
+ ) as jest.Mock
377
+ }
378
+ ```
379
+ MSW(Mock Service Worker): Node.js나 브라우저에서 모두 사용할 수 있는 모킹 라이브러리
380
+
381
+ 브라우저에서는 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 모킹을 구현
382
+
383
+ Node.js 환경에서는 https나 XMLHttpRequest의 요청을 가로채는 방식으로 작동
180
384
### 8.2.4 사용자 정의 훅 테스트하기
385
+ react-hooks-testing-library를 활용해 훅 테스트
181
386
### 8.2.5 테스트를 작성하기에 앞서 고려해야 할 점
387
+ 테스트 커버리지: 해당 소프트웨어가 얼마나 테스트됐는지를 나타내는 지표
388
+
389
+ 테스트 커버리지가 높다고 만능은 아니며 프론트의 경우 TDD로 모든 케이스를 커버하긴 불가능하며, 빠른 진행을 위해 QA에 의존할 수 있음
390
+
391
+ 테스트 코드 최우선 과제는 애플리케이션에서 가장 취약하거나 중요한 부분을 파악하는 것 ex. 결제
182
392
### 8.2.6 그 밖에 해볼 만한 여러 가지 테스트
393
+ - Unit Test: 각각의 코드나 컴포넌트가 독립적으로 분리된 환경에서 의도된 대로 정확히 작동하는지 검증하는 테스트
394
+ - Integration Test: 유닛 테스트를 통과한 여러 컴포넌트가 묶여서 하나의 기능으로 정상적으로 작동하는지 확인하는 테스트
395
+ - End to End Test: E2E 테스트. 실제 사용자처럼 작동하는 로봇을 활용해 애플리케이션의 전체적인 기능을 확인하는 테스트 // Cypress
183
396
### 8.2.7 정리
184
397
185
398
# 09. 모던 리액트 개발 도구로 개발 및 배포 환경 구축하기
@@ -193,15 +406,71 @@ console.log(dom.window.document.querySelector('p').textContent) // "Hello World
193
406
### 9.1.7 정리
194
407
## 9.2 깃허브 100% 활용하기
195
408
### 9.2.1 깃허브 액션으로 CI 환경 구축하기
409
+ CI(Continuous Integration): 여러 기여자가 기여한 코드를 지속적으로 빌드하고 테스트해 코드의 정합성을 확인하는 과정
410
+
411
+ CI는 저장소에서 코드의 변화가 있을 때마다 전체 소프트웨어의 정합성을 확인하기 위한 테스트, 빌드, 정적 분석, 보안 취약점 분석 등의 작업을 자동으로 실행함
412
+
413
+ 과거 젠킨스Jenkins를 사용하였으며 현재 깃허브 액션이 대안으로 사용됨
414
+
415
+ 깃허브 저장소를 기바능로 깃허브에서 발생하는 다양한 이벤트를 트리거 삼아 다양한 작업을 할 수 있게 도와줌
416
+ - 깃허브의 어떤 브랜치에 푸시가 발생하면 빌드를 수행
417
+ - 깃허브의 특정 브랜치가 메인 브랜치를 대상으로 풀 리퀘스트가 열리면 빌드, 테스트, 정적 분석을 수행
418
+ #### 깃허브 액션의 기본 개념
419
+ - 러너: 파일로 작성된 깃허브 액션이 실행되는 서버. 특별히 지정하지 않으면 공용 깃허브 액션 서버를 이용하며, 별도의 러너를 구축해 자체적으로 운영 가능
420
+ - 액션: 러너에서 실행되는 하나의 작업 단위. yaml 파일로 작성된 내용을 하나의 액션이라 볼 수 있음
421
+ - 이벤트: 깃허브 액션의 실행을 일으키는 이벤트. 필요에 따라 한 개 이상의 이벤트 지정 가능. 특정 브랜치를 지정하는 이벤트도 가능.
422
+ - pull_request: PR과 관련된 이벤트. PR이 열리거나, 닫히거나, 수정되거나, 할당되거나, 리뷰 요청되는 등의 PR과 관련된 이벤트
423
+ - issues: 이슈와 관련된 이벤트. 이슈가 열리거나, 닫히거나, 삭제되거나, 할당되는 등 이슈와 관련된 이벤트
424
+ - push: 커밋이나 푸태그가 푸시될 때 발생하는 이벤트
425
+ - schedule: 저장소에서 발생하는 이벤트와 별개로 특정 시간에 실행되는 이벤트를 의미
426
+ - 5 4 * * * : 매일 4시 5분에 실행. 분, 시간, 일, 월, 요일 순으로 표현. * 는 모든 값을 의미
427
+ - jobs: 하나의 러너에서 실행되는 여러 스텝의 모음. 하나의 액션에서 여러 잡을 생성할 수 있으며 특별히 선언한 게 없다면 내부 가상머신에서 각 잡은 병렬로 실행
428
+ - steps: 잡 내부에서 일어나는 하나하나의 작업. 셀 명령어나 다른 액션을 실행할 수도 있음. 병렬X
196
429
### 9.2.2 직접 작성하지 않고 유용한 액션과 깃허브 앱 가져다 쓰기
430
+ #### 깃허브에서 제공하는 기본 액션
431
+ - actions/checkout: 깃허브 저장소를 체크아웃하는 액션. 저장소를 기반으로 작업할 때 반드시 필요. 일반적으로 아무런 옵션없이 사용해 해당 액션을 트리거한 최신 커밋을 불러오지만 ref를 지정해 특정 브랜치나 커밋을 체크아웃할 수 있음
432
+ - actions/setup-node: Node.js를 설치하는 액션. Node.js를 사용하는 프로젝트에 반드시 필요. node.js 버전 지정 가능
433
+ - acitons/github-script: GitHub API가 제공하는 기능을 사용할 수 있도록 도와주는 액션
434
+ - actions/stale: 오래된 이슈나 PR을 자동으로 닫거나 더 이상 커뮤니케이션하지 못하도록 닫음
435
+ - actions/dependency-review-action: 의존성 그래프에 대한 변경. package.json, package-lock.json, pnpm-lock.yaml 등의 내용이 변경됐을 때 실행되는 액션
436
+ - github/codeql-action: 깃허브의 코드 분석 솔루션인 code-ql을 활용해 저장소 내 코드의 취약점을 분석해줌
437
+ #### calibreapp/image-actions
438
+ 저장소에 있는 이미지를 최적화하는 액션
439
+
440
+ PR로 올라온 이미지를 sharp 패키지를 이용해 거의 무손실로 압축해서 다시 커밋해줌
441
+ #### lirantal/is-website-vulnerable
442
+ 특정 웹사이트를 방문해 해당 웹사이트에 라이브러리 취약점이 존재하는지 확인하는 깃허브 액션
443
+ #### Lighthouse CI
444
+ 구글에서 제공하는 액션. 웹 성능 지표인 라이트하우스를 CI를 기반으로 실행할 수 있도록 도와주는 도구
445
+
446
+ 프로젝트의 URL을 방문해 라이트하우스 검사를 실행
197
447
### 9.2.3 깃허브 Dependabot으로 보안 취약점 해결하기
448
+ #### package.json의 dependencies 이해하기
449
+ - 버전
450
+ 유의적 버전semantic vesioning
451
+
452
+ 주.부.수로 구성
453
+ - 기존 버전과 호환되지 않게 API가 바뀌면 주 버전을 올리고,
454
+ - 기존 버전과 호환되면서 새로운 기능을 추가할 때는 부버전을 올리고,
455
+ - 기존 버전과 호환되면서 버그를 수정한 것이면 수 버전을 올린다
456
+ - 의존성
457
+ - dependencies: package.json에서 npm install을 실행하면 설치되는 의존성. npm install 패키지명을 실행하면 dependencies에 추가됨
458
+ - devDependencies: package.json에서 npm install을 실행하면 설치되는 의존성. npm install 패키지명 --save-dev를 실행하면 devDependencies에 추가됨. 프로젝트 실행에는 필요하지 않지만, 개발 단계에서 필요한 패키지들 선언
459
+ - peerDependencies: 주로 서비스보다는 라이브러리와 패키지에서 자주 쓰이는 단위. 직접적으로 해당 패키지를 require하거나 import하지는 않지만 호환성으로 인해 필요한 경우
460
+ #### Dependabot으로 취약점 해결하기
461
+ 의존성에 숨어 잇는 잠재적인 위협을 깃허브를 통해 확인하고 조치하는 방법
198
462
### 9.2.4 정리
199
463
## 9.3 리액트 애플리케이션 배포하기
464
+ 자체적인 IT 인프라가 구축돼 있어 해당 인프라를 사용하는 큰 회사, 비교적 규모가 작아 자체 인프라를 구축하기 어려운 스타트업의 경우 아마존 웹 서비스나 구글 클라우드 플랫폼, 마이크로소프트 애저 등 클라우드 서비스를 활용
465
+
466
+ 개인, 소규모 프로젝트의 경우 대형 클라우드 플랫폼에서 복잡하게 배포 파이프라인을 구축하거나, 혹은 별도의 서버를 마련하지 않더라도 손쉽게 서비스를 배포할 수 있는 다양한 방법이 있음
467
+
200
468
### 9.3.1 Netlify
201
469
### 9.3.2 Vercel
202
470
### 9.3.3 DigitalOcean
203
471
### 9.3.4 정리
204
472
## 9.4 리액트 애플리케이션 도커라이즈하기
473
+ 도커: 서비스 운영에 필요한 애플리케이션을 격리해 컨테이너로 만드는데 이용하는 소프트웨어
205
474
### 9.4.1 리액트 앱을 도커라이즈하는 방법
206
475
### 9.4.2 도커로 만든 이미지 배포하기
207
476
### 9.4.3 정리
0 commit comments