(참고) https://developer.chrome.com/docs/devtools/

Network 패널

  • 네트워크 패널은 자원들이 제대로 다운로드 되었는지의 여부, 캐시여부, 그리고 다운로드된 자원들의 다운로드에 걸린 시간, 세부 정보들을 보고 싶을 때 유용하게 사용할 수 있는 패널임
    • 리소스가 실제로 업로드 또는 다운로드되고 있는지 확인
    • HTTP 헤더, 콘텐츠, 크기 등과 같은 개별 리소스의 속성을 검사
  • 네트워크 활동과 관련이 없는 많은 유형의 로드 성능 문제가 있기 때문에, Lighthouse나 Performance패널을 활용하자
    • 페이지를 더 빠르게 로드하는 방법은 Lighthouse를 참조하자.
    • 성능패널의 런타임 성능은 페이지 로드 중이 아닌 실행 중일 때 성능
    • 참고로 성능분석을 실시할 때는 시크릿 모드에서 수행하는 것이 좋음 (시크릿 모드는 Chrome이 깨끗한 상태로 실행되도록 함. 예를 들어 많은 확장 프로그램이 설치된 경우 해당 확장 프로그램으로 인해 성능 측정에 노이즈가 발생할 수 있음)

Performance 패널

  • 페이지가 로드되는 것이 아니라 실행되는 동안 페이지의 성능을 분석하려는 경우 런타임 성능을 기록함

모바일 CPU 시뮬레이션

  • 모바일 장치는 데스크톱 및 랩톱보다 CPU 성능이 훨씬 낮음
  • 페이지를 프로파일링할 때마다 CPU 스로틀링을 사용하여 모바일 장치에서 페이지가 수행되는 방식을 시뮬레이션해보자.
  1. DevTools에서 성능 탭을 클릭
  2. Screenshots 체크박스가 활성화 되어 있는지 확인
  3. Capture Settings을 클릭 합니다
  4. CPU 의 경우 2x slowdown 을 선택(DevTools는 CPU를 평소보다 2배 느리게 조절)참고 : 다른 페이지를 테스트할 때 저사양 휴대기기에서 제대로 작동하는지 확인하려면 CPU Throttling을 20x slowdown 으로 설정

성능 분석 하는 방법

  • 성능 분석은 웹 페이지가 동작하고 있을 때 자동으로 되는 것이 아니라 특정 구간을 녹화한 후 그 구간을 분석해서 수행하는 식으로 이루어짐
    • 초기 렌더링 성능을 분석하고 싶다면 웹페이지를 불러오기 전에 녹화 버튼을 누르거나 새로고침 버튼을 누르면 됨

1. DevTools에서 Record를 클릭 → DevTools는 페이지가 실행될 때 성능 메트릭을 캡처

2. 몇 초만 기다려보자.

3. 중지 를 클릭 → DevTools는 기록을 중지하고 데이터를 처리한 다음 성능 패널에 결과를 표시

분석결과의 의미

초당 프레임 분석

모든 애니메이션의 성능을 측정하는 주요 메트릭은 초당 프레임 수(FPS) : 사용자는 주로 애니메이션이 60FPS로 실행될 때 만족함

 

1. FPS 차트에서 FPS 위에 빨간색 막대가 표시될 때마다 프레임 속도가 너무 낮아져 사용자 경험에 해를 끼칠 수 있음을 의미하고 일반적으로 녹색 막대가 높을수록 FPS가 높음

그림 1 파란색 윤곽선으로 표시된 FPS 차트

 

2. FPS 차트 아래 에 CPU 차트가 표시됨.  CPU 차트의 색상은 성능 패널 하단에 있는 요약 탭의 색상에 해당 CPU 차트가 색상으로 가득 하다는 사실 은 기록 중에 CPU가 최대로 사용되었음을 의미함 → 오랜 기간 동안 CPU가 최대치에 도달한 것을 볼 때마다 작업을 덜 수행할 수 있는 방법을 찾아야 한다는 신호임

그림 2 파란색 윤곽선으로 표시된 CPU 차트 및 요약 탭

3. FPS , CPU 또는 NET 차트 위로 마우스를 가져가면, DevTools는 해당 시점의 페이지 스크린샷을 보여줌  녹화를 재생하려면 마우스를 좌우로 움직여보자 → 이를 스크러빙이라고 하며 애니메이션 진행을 수동으로 분석하는 데 유용함

그림 3 기록의 2000ms 표시 부근의 페이지 스크린샷 보기

4. 프레임 섹션 에서 녹색 사각형 중 하나 위로 마우스를 가져가면, DevTools는 특정 프레임에 대한 FPS를 보여줌(각 프레임은 아마도 목표인 60FPS보다 훨씬 낮을 것)

그림 4 프레임 위에 마우스 올리기

병목 현상 찾기

애니메이션이 제대로 작동하지 않는다는 것을 측정하고 확인했다면, 이유를 찾아야한다.

 

1. 요약 탭을 확인해보자. 이벤트를 선택하지 않으면 이 탭에 활동 내역이 표시됨  페이지는 대부분의 시간을 렌더링하는 데 사용했다. 

성능은 작업을 적게 하는 기술이므로 렌더링 작업에 소요되는 시간을 줄이는 것이 목표임

그림 1 요약 탭(파란색 윤곽선)

2. 기본 섹션을 확장해보자.  DevTools는 시간 경과에 따른 기본 스레드 활동의 화염 차트를 보여준다. 

x축은 시간 경과에 따른 기록을 나타내고 각 막대는 이벤트를 나타냄 → 넓은 막대는 이벤트가 더 오래 걸렸음을 의미 

y축은 호출 스택을 나타냄 →  이벤트가 서로 쌓여 있는 것을 보면 상위 이벤트가 하위 이벤트의 원인이 되었음을 의미

그림 2 파란색 외곽선으로 표시된 메인 섹션

3. 기록에 많은 데이터가 있다.  FPS , CPU 및 NET 차트 가 포함된 섹션인 개요 위로 마우스를 클릭한 상태로 드래그하여 단일 애니메이션 프레임 실행 이벤트를 확대해보자. → 기본 섹션 및 요약 탭 에는 선택한 녹음 부분에 대한 정보만 표시됨

그림 3 단일 Animation Frame Fired 이벤트 확대

(참고 : 확대/축소하는 또 다른 방법 은 배경을 클릭하거나 이벤트를 선택하여 메인 섹션에 초점을 맞춘 다음 W, A, S 및 D 키를 누르는 것)

 

4. Animation Frame Fired 이벤트 의 오른쪽 상단에 있는 빨간색 삼각형에 주목해보자. → 빨간색 삼각형이 표시될 때마다 이 이벤트와 관련된 문제가 있을 수 있다는 경고임
(참고 : Animation Frame Fired[requestAnimationFrame()](<https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame>) 이벤트는 콜백이 실행될 때마다 발생)

 

5. Animation Frame Fired 이벤트를 클릭해보자 이제 요약 탭에 해당 이벤트에 대한 정보가 표시된다. → 클릭하면 DevTools가 Animation Frame Fired 이벤트 를 시작한 이벤트를 강조표시함 

또한 app.js:94 링크를 클릭하면 소스 코드의 관련 줄로 이동함

그리고 원형 차트가 위치했던 summary 탭이 갱신되면 'reveal' 이라 표시된 링크가 생기고 이 링크를 클릭하면 이를 통해 정확히 어느 위치의 어떤 코드가 해당 이벤트를 발생시키는지를 추적할 수 있음

그림 4 Animation Frame Fired 이벤트에 대한 추가 정보

(참고 : 이벤트를 선택한 후 화살표 키를 사용하여 옆에 있는 이벤트를 선택)

 

6. app.update 이벤트 아래에는 많은 보라색 이벤트가 있을 수 있는데(더 넓으면 각각 빨간색 삼각형이 있는 것처럼 보임), 보라색 레이아웃 이벤트 중 하나를 지금 클릭해보자.

DevTools는 요약 탭 에서 이벤트에 대한 자세한 정보를 제공함.

실제로 강제 리플로우(레이아웃의 또 다른 단어)에 대한 경고가 있음.

 

7. Summary 탭 에서 Layout Forced 아래의 app.js:70 링크를 클릭해보자. → DevTools는 레이아웃을 강제로 적용한 코드 줄로 이동함

그림 5 강제 레이아웃을 유발한 코드 라인

참고 : 이 코드의 문제점은 각 애니메이션 프레임에서 각 사각형의 스타일을 변경한 다음 페이지에서 각 사각형의 위치를 쿼리한다는 것임. 스타일이 변경되었기 때문에 브라우저는 각 사각형의 위치가 변경되었는지 알지 못하므로 위치를 계산하기 위해 사각형을 다시 배치해야 함.(참조 :  강제 동기 레이아웃 방지)

 

성능을 이해하기 위한 Rail 모델

  • RAIL은 성능에 대해 생각할 수 있는 구조를 제공하는 사용자 중심 의 성능 모델임
  • 이 모델은 사용자 경험을 주요 작업(예: 탭, 스크롤, 로드)으로 분류하고 각각에 대한 성능 목표를 정의하는 데 도움을 줌
  • RAIL은 웹 앱 수명 주기의 4가지 뚜렷한 측면인 응답(response), 애니메이션(animation), 유휴 상태(idle) 및 로드(load)를 나타내고 사용자는 이러한 각 컨텍스트에 대해 서로 다른 성능 기대치를 가지고 있으므로 컨텍스트 및 사용자가 지연을 인식하는 방식에 대한 UX 연구를 기반으로 성능 목표가 정의됨
  • 참고 : https://web.dev/rail/

런타임 성능 개선을 위한 팁

출처:
https://developer.chrome.com/docs/devtools/
https://codingmoondoll.tistory.com/entry/크롬-개발자-도구의-Performance-탭-다루기-기본편

Google Lighthouse란?

웹 페이지의 품질을 개선하기 위한 오픈 소스 자동화 도구

공개 또는 인증이 필요한 모든 웹 페이지에 대해 실행 가능

성능, 접근성 Progressive Web Apps, SEO 등에 대해 평가함

  • 성능 : 대화형 시간, 대기 시간, 속도 지수, 리소스 최적화, TTFB, 자산 전달, 스크립트 실행 시간, DOM 크기 등
  • SEO : 모바일 친화적, 메타, 크롤링, 표준, 구조 등
  • 모범사례 : 이미지 최적화, JS 라이브러리, 브라우저 오류 로깅, HTTPS를 통한 액세스, 알려진 JS 취약점 등
  • 접근성 : 페이지 요소, 언어, ARIA 속성 등
  • PWA : HTTP를 HTTPS로 리디렉션, 응답 코드 확인, 3G에서의 빠른 로딩, 스플래시 화면, 뷰포트 등

왜 Lighthouse를 써야하나?

  • 사용하기가 편하다.
  • Google에서 개발했다는 점에서 신뢰도가 있다.
  • 오픈소스이다.
  • 완전히 자동화되어 있다.
  • 스캔한 웹 페이지가 모바일 장치에서 어떻게 보이고 작동하는지도 테스트한다.

어떻게 쓸까?

Chrome 개발자 도구

이것이 이 문서의 메트릭에 대한 스크린샷을 만든 방법입니다.:

  1. 감사할 페이지로 이동합니다.
  2. DevTools(Windows에서는 Ctrl+Shift+I 또는 F12, Mac에서는 Cmd+Option+I)를 엽니다.
  3. 감사 탭으로 이동합니다.
  4. 감사 수행을
  5. 클릭 하고 원하는 범주를 선택합니다.
  6. 감사를 실행합니다.

이는 사용자 인증이 필요한 페이지를 테스트할 때 특히 유용할 수 있습니다.

여기서 흥미로운 점은 Lighthouse 를 Google Chrome 뿐만 아니라 일부 Chromium 기반 브라우저에서도 사용할 수 있다는 것입니다. 예를 들어, 아래는 현재 Google Chrome과 동일한 엔진을 사용하는 최신 버전의 Microsoft Edge에서 가져온 Lighthouse 감사의 스크린샷입니다.

Microsoft Edge 브라우저에서 시작된 Google Lighthouse 감사의 스크린샷.

Lighthouse를 노드 모듈로 실행

이렇게 하면 명령줄에서 감사를 실행하고 감사 결과가 포함된 * .html 파일을 얻을 수 있습니다.

  1. 컴퓨터에 Google 크롬이 설치되어 있는지 확인하세요.
  2. Node의 현재 장기 지원 버전을 설치합니다 (다음 예제는 최신 비 LTS 버전으로 수행되었지만 Google 자체에서 LTS 버전 사용을 권장함).
  3. 다음 명령을 사용하여 Lighthouse를 전체적으로 설치합니다. npm install -g lighthouse
  4. lighthouse 명령으로 " https://google.com" 데모 감사를 실행해 봅시다 .
  5. 감사 목적으로 Chrome 창이 자동으로 나타납니다. 생성된 보고서는 현재 폴더에 저장됩니다.

감사를 실행할 때 Lighthouse는 현재 테스트 단계(왼쪽)를 인쇄하고 완료되면 보고서 데이터(오른쪽)가 포함된 HTML 파일을 생성합니다.

프로그래밍 방식으로 Lighthouse 모듈 실행

통합 프로세스를 실행 중인 경우 Lighthouse를 모듈로 사용하여 자동화된 테스트를 실행할 수 있습니다. 예를 들면 다음과 같습니다.

const fs = require('fs');
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

(async () => {
  const chrome = await chromeLauncher.launch({chromeFlags: ['--headless']});
  const options = {logLevel: 'info', output: 'html', onlyCategories: ['performance'], port: chrome.port};
  const runnerResult = await lighthouse('<https://example.com>', options);

  // `.report` is the HTML report as a string
  const reportHtml = runnerResult.report;
  fs.writeFileSync('lhreport.html', reportHtml);

  // `.lhr` is the Lighthouse Result as a JS object
  console.log('Report is done for', runnerResult.lhr.finalUrl);
  console.log('Performance score was', runnerResult.lhr.categories.performance.score * 100);

  await chrome.kill();
})();

성능지표?

지각적으로 페이지의 속도를 측정

(해당 측정항목 및 해당 가중치)

  • First Contentful Paint—15%
  • Speed Index—15%
  • Largest Contentful Paint—25%
  • Time to Interactive—15%
  • Total Blocking Time—25%
  • Cumulative Layout Shift—5%

First Contentful Paint

  • FCP라고 하는 이 지표는 브라우저가 DOM 콘텐츠를 렌더링하는 데 걸리는 시간을 보여줌
  • 이 경우 DOM 콘텐츠는 텍스트, 이미지, 흰색이 아닌 <canvas>요소 및 SVG
  • 이 측정항목은 로드 시간이 아니라 렌더링 시간을 표시 한다는 점을 이해하는 것이 중요
    • 예를 들어, 브라우저가 특정 텍스트를 로드했지만 글꼴이 여전히 네트워크를 통해 이동 중인 경우 여전히 콘텐츠가 있는 페인트로 간주되지 않는다.
  • 이 도구는 색상 코딩 시스템을 사용하여 특정 메트릭에 따라 페이지가 얼마나 잘 수행되는지 표시함.
    • 메트릭 근처의 원은 빨간색, 주황색 또는 녹색(각각 느림, 보통 또는 빠름)일 수 있습니다. 해석은 다음과 같다.
    • 2초 미만의 FCP 시간 - 녹색, 빠름
    • 2~4초의 FCP 시간—주황색, 보통
    • 4초 이상의 FCP 시간 - 빨간색, 느림

속도지수

  • 이것은 페이지의 콘텐츠가 시각적으로 얼마나 빨리 로드되는지 보여줌
  • 이를 위해 Lighthouse는 페이지 로드 비디오를 녹화한 다음 프레임 간의 시각적 진행을 계산함
  • 따라서 페이지 요소가 빠르게 표시되는 경우(하지만 여전히 백그라운드에서 로드되는 스크립트가 있을 수 있음) 이 메트릭은 안전 영역에 있습니다.
  • Lighthouse는 이 지표를 사용하여 페이지 속도를 다른 웹사이트와 비교하며, 조건은 다음과 같다.
    • 4.3초 미만의 속도 지수 - 녹색, 빠름
    • 4.4~5.8초의 속도 지수—주황색, 보통
    • 5.8초 동안의 속도 지수—빨간색, 느림

Largest Contentful Paint

  • LCP는 성과 점수 계산기에서 중요한 역할을 함
  • 뷰포트(즉, 페이지의 보이는 부분) 내에서 가장 큰 이미지 또는 텍스트 블록 의 렌더링 시간을 보고함
  • 메트릭을 트리거할 후보로 간주되는 요소:
    • <img>집단
    • <image><svg> 요소 내부 의 요소
    • <video>요소(이 경우 포스터 이미지 사용)
    • CSS에서 url()을 통해 로드된 배경 이미지가 있는 요소
    • 자식이 있는 블록 수준 요소(텍스트 노드도 포함) - 여백, 패딩 및 테두리는 무시됨
  • 가장 큰 요소는 페이지가 점진적으로 로드됨에 따라 변경될 수 있고 가장 큰 요소 캡처의 마지막 레코드가 보고됨
    • LCP 2.5초 미만 - 녹색, 빠름
    • 2.5~4초의 속도 지수—주황색, 보통
    • 4초 동안의 속도 지수—빨간색, 느림

Time To Interactive

  • 페이지가 완전히 상호 작용 하는 데 걸리는 시간을 측정
  • 이 경우 "완전히"의 의미는 다음과 같다.
    • 유용한 콘텐츠(FCP로 측정)가 표시됨
    • Javascript 이벤트 핸들러는 보이는 요소의 이벤트에 바인딩됨
    • 페이지는 50밀리초 이내에 사용자 상호 작용에 응답함
  • 비교 데이터는 HTTP Archive 에서 가져옴
    • 3.9초 미만의 TTI - 녹색, 빠름.
    • 3.9~7.3초의 TTI—주황색, 보통
    • 7.3초 이상의 TTI—빨간색, 느림

Total Blocking Time

  • 페이지가 50밀리초 이상 사용자 상호 작용에서 차단될 때 FCP와 TTI 사이의 모든 시간 기록의 합계
    • 예를 들어 일부 Javascript 코드가 70밀리초 동안 페이지 로드를 중단하면 TBT가 20밀리초 증가함
  • Lighthouse의 소스 코드에 따르면 TBT 점수는 상위 10,000개 웹사이트와 비교하여 계산됨
    • 300밀리초 미만의 TBT - 녹색, 빠름.
    • 300~600밀리초의 TBT—주황색, 보통
    • 600밀리초 이상의 TBT - 빨간색, 느림

Cumulative Layout Shift

  • CLS는 요소가 서로 얼마나 적극적으로 이동하는지 알려줄 수 있음
    • 예를 들어 긴 텍스트를 읽고 있다고 가정할 때, 읽고 있는 부분 위에 요소가 렌더링되면 텍스트가 아래로 이동함. 이와 같은 상황을 제거하면 CLS 점수에 긍정적인 영향을 줌
  • 이 지표는 예상치 못한 레이아웃 변경만 고려하며, 예상치 못한 레이아웃 변경은 변경 전 500ms 시간 프레임 내에 사용자 입력이 없는 레이아웃 변경입니다.

CI?

(참고) GoogleChrome/lighthouse-ci GitHub

  • 팀에서 지속적 통합 워크플로를 사용하는 경우 Google Lighthouse CI 도구 세트를 사용하여 워크플로의 일부로 Google Lighthouse를 실행할 수 있음
  • Google Lighthouse CI는 CI 워크플로우에서 Google Lighthouse 점수 실행을 단순화하는 도구 모음임
  • Lighthouse CI는 Circle CI, GitHub 작업 및 Travis CI와 같은 CI 공급자와 함께 작동함

1단계 - GitHub Actions 워크플로 디렉터리

  • .github/workflowGitHub 작업 사용을 시작하려면 프로젝트 디렉터리의 루트에 GitHub 워크플로 파일을 저장할 디렉터리를 만들어야 한다.

2단계 - GitHub Actions 워크플로 파일

  • 특정 이벤트가 발생할 때 Google Lighthouse CI를 실행하기 위한 코드를 포함할 YAML 워크플로 파일을 만든다.
  • 이벤트가 발생 (push, pull_request)하면 Google Lighthouse CI를 실행 하도록 하자.
  • lighthouse.yaml 이라는 YAML 파일을 만들자.
  • 프로젝트 디렉터리의 루트에 HTML 파일이 있는 가장 간단한 경우를 살펴보자.
  • 또한 프로젝트 디렉터리의 루트에 configuration file이 없다고 가정한다.
  • 방금 만든 YAML 파일에 아래 코드를 복사하여 붙여넣어보자.
  • 변경 사항을 Git 리포지토리로 푸시하거나 풀 요청을 열 때마다 코드가 실행된다.
  • 프로젝트에 빌드 단계가 필요한 경우엔 워크플로 파일을 약간 수정할 수 있다.
name: Run lighthouse CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lhci:
    name: Lighthouse CI
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [14.x]

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with:
          node-version: ${{ matrix.node-version }}
      - name: Run lighthouse CI
        run: |
          npm install -g @lhci/cli@0.3.x && lhci autorun --upload.target=temporary-public-storage --collect.staticDistDir=./

3단계 - Push the changes to GitHub

  • 위의 코드는 변경 사항을 커밋하고 GitHub에 푸시할 때 Google Lighthouse CI를 실행한다.
  • 누군가 풀 요청을 열거나 커밋을 푸시할 때마다 GitHub 작업은 지정된 명령을 실행하므로 시간 경과에 따른 회귀를 알 수 있다.
  • Circle CI 및 Travis CI와 같은 다른 CI 도구와 함께 Google Lighthouse를 실행할 수 있으며, Google Lighthouse CI 문서는 지원되는 모든 CI 제공자를 안내하고 있다.

참고자료

사이트 성능을 높이기 위한 기술들을 다룬 블로그
https://web.dev/fast/
https://yceffort.kr/2021/08/javascript-tree-shaking

출처 :
개요
https://developer.chrome.com/docs/lighthouse/overview/
성능지표 설명 및 사용법
https://uploadcare.com/blog/what-is-google-lighthouse/#largest-contentful-paint
사용 시 장점
https://medium.com/@OPTASY.com/how-good-of-website-analysis-tool-is-google-lighthouse-6-reasons-to-use-it-e86adc5d505b
CI
https://refine.dev/blog/lighthouse-google-chrome/

Front-End Tesing이란?

  • GUI(Graphical User Interface), 웹 애플리케이션 또는 소프트웨어의 기능 및 유용성을 테스트하는 테스트 기술 (최종 사용자에게 표시되는 메뉴, 양식, 버튼 및 기타 애플리케이션 요소의 유효성 검사가 포함)
  • 프런트 엔드 테스트의 목표는 전반적인 기능을 테스트하여 웹 애플리케이션 또는 소프트웨어의 프레젠테이션 계층이 연속적인 업데이트로 결함이 없는지 확인하는 것
  • 위의 프런트엔드 테스트 외에도 다음을 위해 수행됨
    • CSS 회귀 테스트: 프런트엔드 레이아웃을 깨는 사소한 CSS 변경
    • 프런트엔드를 작동하지 않게 만드는 JS 파일 변경
    • 성능 확인

Front-End Test는 왜 할까?

안전한 프로덕션 배포(예: 해당 앱이 단순히 작동)를 보장하고 싶고, 사용자 상호 작용의 전체 주기 동안 애플리케이션이 안정적으로 유지되는지 확인하고 싶을 수도 있음

1. 클라이언트 측 성능 문제 감지

이것이 중요하지만 프런트 엔드 테스트를 통해 사용자 관점에서 서비스 테스트 가능

프런트 엔드 테스트를 통해 클라이언트 측의 문제를 정확히 확인하고 애플리케이션의 중요한 워크플로우의 안정성을 확인할 수 있음

사용성, 탐색 및 페이지 로드 속도와 같은 요소는 사용자와 검색엔진의 순위 알고리즘 모두에 중요

성능이 낮은 UI는 특히 중요한 워크플로가 손상된 경우 리드 또는 수익 창출 채널에 피해를 줄 수 있음

애플리케이션 프런트 엔드의 미묘한 오류가 돌이킬 수 없는 손상을 일으킬 수 있다는 것임

최종 사용자보다 먼저 시스템의 결함을 발견해야 하며 여기에서 프런트 엔드 테스트가 시작됨

즉, 무한 로딩 시간 또는 사용자가 종료할 수 없는 오류 상태와 같은 영역을 포함할 수 있는 애플리케이션의 취약하거나 중요한 부분에서 엣지 케이스를 보호해야 함

이러한 사항을 확인하기 위해 수행하는 테스트는 특정 요구 사항에 따라 수동 또는 자동일 수 있음

2. 다양한 브라우저 및 시스템에서 애플리케이션 동작 검증

프런트 엔드 테스트는 다양한 운영 체제, 브라우저 및 장치에서 웹 애플리케이션의 동작을 확인할 때 중요한 역할을 함

프런트 엔드를 테스트할 수 있는 수많은 브라우저와 OS 조합이 있음

다양한 시스템 아키텍처에서 애플리케이션의 기능 및 응답성을 검증하는 데 도움이 됨

이는 브라우저 기술의 수정과 결합된 클라이언트 측 개발의 발전으로 인해 호환성 문제가 발생할 수 있기 때문에 특히 중요함

따라서 프런트 엔드 테스트는 웹 사이트 또는 애플리케이션이 다른 장치 및 브라우저 엔진에서 동일하게 렌더링되는지 확인하는 데 필요

3. 사용자 상호 작용 및 경험의 품질 향상

프런트 엔드 테스트는 개발 팀이 이러한 성능 벤치마크를 최적화하여 사용자에게 더 나은 경험을 제공하는 데 도움이 됨

보다 구체적으로, 애플리케이션 로드 시간을 줄이고 애플리케이션의 콘텐츠가 올바르게 표시되도록 하며 다양한 장치 및 브라우저에서 인터페이스에 통합된 모양을 제공할 수 있음

이러한 클라이언트측 요소를 테스트하고 개선하면 애플리케이션의 품질이 기하급수적으로 향상될 수 있고 사용자는 다양한 환경에서 더 좋고 일관된 경험을 즐길 수 있을 것

4. 타사 서비스의 원활한 통합 보장

거의 모든 최신 애플리케이션은 어느 시점에서 타사 서비스와의 통합이 필요할 수 있음

특히 SaaS(Software as a Service) 플랫폼이 점차 인기를 얻고 있는 지금에는 그 가능성이 더 높음

애플리케이션에 다른 서비스를 통합할 때 성능이 좋지 않은 스크립트로 인해 손상될 수 있음

이는 사용자가 애플리케이션과 상호 작용할 때 사용자 경험에 상당한 피해를 줄 수 있기 때문에, 타사 서비스를 웹 애플리케이션에 통합하려는 사람에게는 프런트 엔드 테스트가 필수적임

5. 여러 개발자가 참여하는 프로젝트에서 코드 신뢰성 보장

최신 애플리케이션의 복잡성이 증가함에 따라 대규모 프로젝트를 단독으로 제공할 수 있는 경우는 거의 없음

누구도 다른 프로그래머가 작성한 모든 코드 조각에 대해 모든 것을 알 수는 없으며, 이것이 코드가 일관성이 없고 프런트엔드 기능이 손상되지 않았는지 다시 확인하는 프런트엔드 테스트가 중요한 이유임

6. 테스트 문서의 유효성

잘 관리된 테스트를 유지해야 하는 또 다른 좋은 이유는 테스트가 실제 문서로 제공되기 때문

테스트를 작성하려면 특정 테스트(및 관련된 애플리케이션의 구성 요소)가 수행하는 작업에 대한 적절한 설명이 필요합니다.

적절한 테스트를 실행하려면 구성 요소의 API를 사용하여 모의를 추가해야 하고, 나중에 다른 개발자나 팀이 나중에 어떻게 사용할 수 있는지에 대한 지침이 될 수 있음

7. 코드 가독성 향상 및 결합도를 낮출 수 있음

test suite를 작성하면 애플리케이션 코드의 가독성을 높이고 결합도를 낮출 수 있음

개발자가 응용 프로그램의 작은 청크를 테스트하고 싶지만 테스트와 반드시 관련되지 않은 몇 가지 종속 구성 요소 및 모의를 스핀업해야 하는 경우 이는 코드의 일부를 리모델링해야 한다는 신호일 수 있음 (⇒ 코드 부분 간의 상호 의존성이 너무 빡빡하다는 신호)

더 깨끗한 코드는 더 테스트하기 쉽고 테스트 가능한 코드는 더 깨끗함 ⇒ 이는 프런트엔드 개발자 와 궁극적으로 애플리케이션의 최종 사용자에게 윈-윈 시나리오임

결론

  • 프런트엔드 테스트는 프런트엔드 기능, GUI 및 사용성을 테스트하거나 확인하는 것
  • 프런트엔드 테스트의 주요 목표는 모든 사용자가 버그로부터 잘 보호되는지 확인하는 것
  • 프런트엔드 테스트 계획을 작성하면 프로젝트에서 다루어야 하는 장치, 브라우저 및 시스템을 파악하는 데 도움이 됨
  • 또한 프로젝트 범위에 대한 완전한 명확성을 얻는 데도 도움이 됨

Front End Testing 계획

테스트 시, 집중해야 할 몇 가지 측면

애플리케이션의 프런트엔드를 테스트할 때 집중해야 할 몇 가지 측면이 있음

  • 브라우저 간 및 플랫폼 간 기능
    • 다양한 브라우저, 플랫폼 및 장치에서 앱의 기능과 응답성을 모두 확인
  • 접근성
    • 시각 또는 청각 장애가 있는 사람을 포함하여 모든 사람이 애플리케이션에 액세스할 수 있는지 확인
  • end-to-end 확인
    • 사용자가 취할 가능성이 있는 실제 작업을 모방하여 애플리케이션의 end-to-end 워크플로(백엔드에서 프런트엔드로)를 확인하고 확인하는 데 필요
  • 이미지 분석 테스트
    • 요즘 대부분의 웹사이트와 앱에는 표준 디스플레이 이미지에서 로고, 인포그래픽 및 배너에 이르기까지 많은 이미지가 있음. 애플리케이션의 크기가 크게 증가하므로 테스트를 실행하여 앱이 더 빠르게 실행되도록 이미지를 최적화할 수 있는 위치를 확인해야 함
  • CSS(Cascading Style Sheets) 테스트
    • 두 가지 주요 CSS 요소인 구문 및 디스플레이의 성능을 보장하기 위해 테스트를 실행해야 함.

계획의 4단계

1단계) 테스트 계획 관리를 위한 도구 찾기

2단계) 프런트 엔드 테스트를 위한 예산 결정

3단계) 전체 프로세스의 타임라인 설정

4단계) 프로젝트의 전체 범위를 결정(범위에는 다음 항목 포함)

  • 사용자가 사용하는 OS 및 브라우저 사용자의 ISP (*ISP : 인터넷 서비스 제공자)
  • 사용자들이 많이 사용하는 기기
  • 사용자의 숙련도
  • 사용자의 인터넷 수정 속도

FIRST 원칙

프런트엔드 테스트는 중요하지만 테스트를 실행할 때 모범 사례를 보장하기 위해 특정 원칙을 고수하는 것도 중요

그렇지 않으면 테스트 결과를 완전히 신뢰하지 못할 수 있음

프런트엔드 테스트의 모범 사례를 고수하려면 따라야 할 프레임워크가 필요한데, FIRST 원칙을 사용가능함

FIRST 원칙은 다음을 의미합니다.

  • Fast : 빠른
  • Independent : 독립적인
  • Repeatable : 반복 가능한
  • Self-validating : 자체 검증 가능한
  • Thorough & Timely : 철저하고 적시에

테스트는 신속하게(수명 주기의 필요한 시점에서) 실행되어야 하고, 테스트되지 않은 구성 요소와 격리되어야 하며, 미래에 쉽게 반복할 수 있어야 하고, 테스트 통과 여부를 스스로 검증할 수 있어야 하며 필요한 모든 변수를 다룰 수 있어야 함

프런트엔드 요소의 우선 순위 지정

프런트엔드 테스트는 수백 또는 수천 개의 UI 및 기능 요소를 분석하고 확인하는 것을 의미

UI 요소에는 서식, CSS, 텍스트, 그래픽 등이 포함되며 기능 요소에는 양식, 링크, 버튼 등이 포함됨

효과적인 테스트 프로세스를 보장하려면 먼저 테스트할 항목의 우선 순위를 지정해야 함

페이지 로드 속도, 기본 텍스트, 이미지 및 필수 기능(예: 장바구니에 항목 추가, 결제 도구)을 먼저 테스트하고 그래픽 및 팝업으로 이동하기 전에 테스트하는 것이 합리적일 것

이러한 각 요소가 표시되고 반응하는지 확인한 다음 그래픽 및 레이아웃 확인으로 이동

실제 브라우저 및 장치 사용

실제 브라우저와 장치를 사용하는 것은 오류 없이 실제 환경을 최대한 반영하는 신뢰할 수 있는 프런트엔드 테스트를 수행하는 데 필수적임

에뮬레이터 및 시뮬레이터 사용을 피하고 실제 브라우저 및 장치를 사용하여 시간과 리소스를 절약하면 소프트웨어 테스트 결과를 훨씬 더 신뢰할 수 있음

테스트를 위한 팁

  • 예산, 자원 및 시간을 현명하게 준비
  • 테스트가 더 빨리 실행되도록 헤드리스 브라우저를 사용
  • 더 빠른 실행을 위해 테스트에서 DOM 렌더링의 양을 줄이기
  • 테스트 사례를 격리하여 버그의 근본 원인을 신속하게 파악하여 더 빠른 결함 수정 주기
  • 더 빠른 회귀 주기를 위해 테스트 스크립트를 재사용이 가능하게 만들기
  • 테스트 스크립트에 일관된 명명 규칙을 사용

프런트엔드 테스트 유형

프런트엔드에 대해 테스트할 여러 요소가 있으므로 실행을 고려할 수 있는 몇 가지 다른 유형의 테스트가 있고 이들 각각은 프런트엔드의 서로 다른 구성 요소에 초점을 맞추고 있음

단위 테스트

단위 테스트는 프런트엔드 테스트의 기본 빌딩 블록임

개별 구성 요소와 기능을 분석하여 예상대로 작동하는지 확인

이는 모든 프런트엔드 애플리케이션에 매우 중요하며 프로덕션 환경에서 예상되는 작동 방식에 대해 구성 요소 및 기능을 테스트하여 고객을 위한 안정적인 코드베이스와 신뢰할 수 있는 앱으로 이어짐

에지 케이스 및 테스트 API와 같은 항목에 단위 테스트를 사용할 수도 있음

Acceptance 테스트

수락 테스트는 사용자 입력, 사용자 흐름 및 프런트엔드의 지정된 작업이 코딩되고 제대로 작동하는지 확인하기 위해 수행됨

애플리케이션의 최종 모델이 최종 사용자가 기대하는 대로 작동하는지 확인하기 위해 이를 수행

Visual Regression(시각적 회귀) 테스트

시각적 회귀 테스트는 고유한 프런트엔드 테스트임

다른 유형의 테스트는 코드에 중점을 두므로 백엔드 스택에 대해서도 실행할 수 있음

차례로 시각적 회귀 테스트는 응용 프로그램의 실제/기존 인터페이스를 해당 '예상' 버전과 비교하여 차이를 식별함

이는 헤드리스, 서버 실행 브라우저의 스크린샷을 비교하여 이루어지며, 머신을 사용하여 스크린샷 간의 이미지 비교를 수행하고 차이점을 식별하고 강조 표시함

접근성 테스트

접근성 테스트는 시각 장애가 있거나 기타 추가 요구 사항이 있는 개인을 포함하여 모든 잠재적 사용자가 응용 프로그램이나 웹 사이트를 쉽게 사용할 수 있는지 확인함

때때로 사용성 테스트의 하위 범주로 간주되며 특정하고 변경 불가능한 조건으로 인해 앱의 기능에 액세스하는 데 방해가 되지 않고 다른 사람처럼 쉽게 인터페이스를 탐색할 수 있는지 확인함

성능 시험

성능 테스트는 속도, 안정성, 확장성, 상호 운용성 및 응답성을 포함한 특정 매개변수 내에서 애플리케이션의 성능을 분석함

사용자 로드가 증가할 때 제품이 원하는 품질을 유지하고 사용자 요청 및 작업에 빠르고 신속하게 응답하는지 확인하는 데 도움이 되므로 프런트엔드 테스트에 중요함

종단간(E2E) 테스트

종단 간 테스트는 응용 프로그램의 흐름이 처음부터 끝까지 예상대로 작동하는지 확인하고 확인하는 데 사용됨

주로 실제 시나리오 내에서 실제 사용자의 작업을 모방하여 응용 프로그램의 인터페이스와 API 간의 원활한 통신이 원활하게 실행되도록 함

이렇게 하면 함께 결합된 여러 시스템 요소의 결합된 동작에 대한 통찰력을 얻을 수 있음

통합 테스트

대부분의 최신 애플리케이션은 다양한 모듈로 구축됨

이러한 모듈이 제대로 통합되지 않고 함께 잘 작동하지 않으면 최종 사용자 경험을 망칠 수 있음

모든 것이 효과적으로 함께 작동하는지 확인하려면 통합 테스트를 실행해야 함

브라우저 간 테스트

브라우저 간 테스트는 응용 프로그램이 다른 웹 브라우저에서 예상대로 작동하는지 확인하기 위해 수행됨

이 프로세스에는 서로 다른 브라우저에서 동일한 테스트 케이스 세트를 실행하여 애플리케이션이 각 브라우저에서 호환되는지 확인하는 작업이 포함됨

이러한 테스트는 매번 동일하므로 이 프로세스를 자동화할 수 있음

Front-End 테스트 도구

Jest

Jest는 단순성에 중점을 둔 가장 인기 있는 JavaScript 테스트 프레임워크 중 하나

테스트에 고유한 전역 상태가 있는지 확인함으로써 Jest는 테스트를 병렬로 안정적으로 실행할 수 있음

작업을 빠르게 하기 위해 Jest는 이전에 실패한 테스트를 먼저 실행하고 테스트 파일이 걸리는 시간에 따라 실행을 재구성함

또한 강력한 코드 커버리지와 손쉬운 조롱 도구를 제공

Selenium WebDriver

Selenium WebDriver는 개발자가 브라우저 간 테스트를 실행할 수 있는 웹 프레임워크임

호환성을 확인하기 위해 웹 기반 애플리케이션 테스트를 자동화하는 데 사용

이 도구를 사용하면 프로그래밍 언어를 선택하여 브라우저 간 테스트를 위한 테스트 스크립트를 만들 수 있음

크로스 브라우저 테스트, 웹 테스트 및 웹 사이트의 올바른 기능이 확인되었는지 확인하는 데 효과적으로 사용할 수 있음

자동화된 스크립트는 다양한 플랫폼과 여러 브라우저에서 웹 애플리케이션용으로 사용자가 작성할 수 있음

많은 플러그인과 녹음 및 재생 솔루션을 제공하며, 브라우저와 직접 상호 작용하여 효율적이고 빠름

Cypress

Cypress는 웹 테스트 자동화를 위한 종단 간 테스트 프레임워크

단위 테스트, 통합 테스트, 종단 간 테스트와 같은 다양한 테스트를 효율적으로 작성할 수 있음

이를 통해 프런트엔드 개발자는 JavaScript로 자동화된 웹 테스트를 작성할 수 있음

Cypress Syntax의 사용편의성이 좋음

Cypress는 브라우저 내부에서 직접 작동할 수 있습니다. 브라우저의 동작을 수정할 수 있으며 인터페이스를 통해 오류를 쉽게 찾을 수 있음

WebDriverIO

WebdriverIO는 최신 웹 및 모바일 애플리케이션을 자동화하기 위해 구축된 진보적인 자동화 프레임워크임

앱과의 상호 작용을 단순화하고 확장 가능하고 강력하며 안정적인 test suite를 만드는 데 도움이 되는 플러그인 세트를 제공

NodeJS를 기반으로 구축되었으며 JavaScript 언어로 작성되었음.

간결한 스크립트 작성 기능이 제공되며 구조가 간단함

또한 타사 테스트 솔루션 제공업체와 쉽게 통합할 수 있음

친숙한 방식으로 프런트 엔드 테스트를 제공함

WebDriverJS

WebDriverJs는 Selenium의 Json-wire-protocol을 사용하여 브라우저와 상호 작용하는 Selenium의 공식 Javascript 버전임

WebDriverJS는 기본적으로 Selenium WebDriver와 동일한 기능을 수행함

Test Cafe

테스트 카페는 노드입니다. 웹 애플리케이션을 테스트하는 데 사용할 수 있는 Node.js 종단간 무료 오픈 소스 자동화 도구입니다. Windows, MacOS 및 Linux와 같은 널리 사용되는 모든 환경에서 작동합니다. 단일 명령의 설치하기 쉬운 기능을 사용하여 JavaScript 또는 TypeScript로 스크립트를 작성할 수 있습니다.

Lambda Test

가능한 모든 측면에서 웹 제품을 검사할 수 있도록 수많은 신규 및 레거시 모바일 및 데스크탑 브라우저와 OS를 제공하는 크로스 브라우저 테스트 도구

다양한 플랫폼에서 수동 테스트, 자동화 테스트, 지리적 위치 테스트, 사이프러스 테스트를 수행할 수 있으며 테스트 결과 스크린샷을 번거로움 없이 팀과 공유할 수 있음

팀 커뮤니케이션의 디버깅 및 품질 향상을 위해 많은 통합이 제공됨

Katalon Studio

모바일, 데스크톱 API 및 웹 UI 테스트를 제공하는 테스트 자동화 도구

테스트 생성은 다재다능하며 코딩 경험이 있거나 없는 사용자를 위한 이중 편집기 인터페이스와 함께 제공됨

여러 로케이터 전략으로 UI 변경을 완벽하게 조정할 수 있음

객체 탐지기의 불안정성을 처리하기 위해 자가 치유 메커니즘을 제공

각 실행 후 실시간 알림 및 통찰력 있는 그래프로 보고서를 생성할 수 있음

Test Complete

모바일, 웹 및 데스크톱 애플리케이션을 테스트하는 GUI 자동화 도구

비기술 사용자와 기술 사용자 모두 사용할 수 있음

애플리케이션의 품질은 효율성과 규모로 제공됨

코드리스 또는 코드 테스트 생성을 제공

복잡한 물체를 식별하고 뛰어난 물체 인식 기능을 제공

Front End 성능 최적화

이전의 성능 최적화는 서버 측 최적화를 의미했음

대부분의 웹사이트가 대부분 정적이었고 대부분의 처리가 서버 측에서 이루어졌기 때문

그러나 Web 2.0 기술의 시작과 함께 웹 애플리케이션이 더욱 동적으로 변하면서, 클라이언트 측 코드는 성능을 많이 차지하게 되었음

프런트 엔드 성능 최적화하면 뭐가 좋은데?

  • 웹 사이트 테스트에서 서버 병목 현상을 제외하고 클라이언트 측 성능 문제를 찾는 것은 사용자 경험에 쉽게 영향을 미치기 때문에 똑같이 중요
  • 백엔드 성능을 50% 향상시키면 애플리케이션의 전체 성능이 10% 향상되지만, 프런트 엔드 성능을 50% 향상시키면 애플리케이션의 전체 성능이 40% 향상됨
  • 또한 프런트엔드 성능 최적화는 백엔드보다 쉽고 비용 효율적임

예제를 통해 알아보자!

단위 테스트

단위 테스트는 테스트를 위한 가장 기본적인 빌딩 블록

개별 구성 요소를 살펴보고 예상대로 작동하는지 확인

이러한 종류의 테스트는 모든 프런트 엔드 애플리케이션에 중요

이를 통해 구성 요소가 예상되는 동작에 대해 테스트되어 훨씬 더 안정적인 코드베이스와 앱으로 이어지기 때문

이것은 또한 엣지 케이스와 같은 것을 고려하고 다룰 수 있는 곳임

단위 테스트는 API 테스트에 특히 유용함

그러나 라이브 API를 호출하는 대신 하드코딩된(또는 "모의") 데이터를 통해 테스트 실행이 항상 일관되게 유지됨

(간단한 예)

const sayHello = (name) => {
  if (!name) {
    return "Hello human!";
  }return `Hello ${name}!`;
};

이것은 기본적인 경우이지만 누군가가 애플리케이션에 이름을 제공하는 것을 무시했을 수 있는 작은 극단적인 경우를 다루고 있음

"왜 그렇게 작은 것을 테스트해야 하나?"

  • 당신의 기능의 가능한 결과에 대해 깊이 생각하도록 강요하며, 대부분의 경우 코드에서 이를 커버하는 데 도움이 되는 에지 케이스를 실제로 발견함
  • 코드의 일부는 이 엣지 케이스에 의존할 수 있으며 누군가 와서 중요한 것을 삭제하면 테스트는 이 코드가 중요하며 제거할 수 없다고 경고함

단위 테스트는 종종 작고 단순함

describe("sayHello function", () => {
  it("should return the proper greeting when a user doesn't pass a name", () => {
    expect(sayHello()).toEqual("Hello human!")
  })it("should return the proper greeting with the name passed", () => {
    expect(sayHello("Evgeny")).toEqual("Hello Evgeny!")
  })
})

describe테스트를 터미널에 인쇄되는 논리 블록 으로 it나눔

가장 중요한 줄은 expect및 toEqual임

이 expect함수는 유효성을 검사하려는 입력을 toEqual수락하고 원하는 출력을 수락함

응용 프로그램을 테스트하는 데 사용할 수 있는 다양한 기능과 메서드가 많이 있음

단위 작성을 위한 라이브러리인 Jest 로 작업하고 있다고 가정해 보았을 때, 위의 예에서 Jest는 sayHello함수를 터미널에 제목으로 표시함

함수 내부의 모든 것은 it단일 테스트로 간주되며 함수 제목 아래의 터미널에 보고되므로 모든 것을 매우 쉽게 읽을 수 있음

녹색 확인 표시는 두 테스트 모두 통과했음을 의미

통합 테스트

단위 테스트가 블록의 동작을 확인하는 경우 통합 테스트는 블록이 함께 완벽하게 작동하는지 확인

이는 구성 요소 간의 테스트 상호 작용을 열어주기 때문에 통합 테스트를 매우 중요하게 만듬

애플리케이션이 자체적으로 작동하는 분리된 부분으로 구성되는 경우는 매우 드뭄

이것이 우리가 통합 테스트에 의존하는 이유임

(간단한 예)

이번에는 간단한 React 애플리케이션으로 예시를 들어보자

버튼을 클릭하면 인사말이 화면에 표시된다고 가정해 보자

즉, 테스트에는 기능뿐만 아니라 HTML DOM 및 버튼의 기능도 포함됨

우리는 이 모든 부분이 어떻게 함께 작동하는지 테스트하고 싶음

<Greeting />테스트 중인 구성 요소 의 코드는 다음과 같음

export const Greeting = () => {
  const [showGreeting, setShowGreeting] = useState(false);return (
   <div>
     <p data-testid="greeting">{showGreeting && sayHello()}</p>
     <button data-testid="show-greeting-button" onClick={() => setShowGreeting(true)}>Show Greeting</button>
   </div>
 );
};

다음은 통합 테스트임

describe('<Greeting />', () => {
  it('shows correct greeting', () => {
    const screen = render(<Greeting />);
     const greeting = screen.getByTestId('greeting');
     const button = screen.getByTestId('show-greeting-button');expect(greeting.textContent).toBe('');
     fireEvent.click(button);
     expect(greeting.textContent).toBe('Hello human!');
 });
});

우리는 단위 테스트에서 describe이미 알고 있음

it테스트를 논리적 부분으로 나눔

특수하게 emulate된 DOM에  <Greeting /> 컴포넌트를 표시하는 render라는 기능이 있으므로 실제 DOM을 건드리지 않고 구성 요소와의 상호 작용을 테스트할 수 있음 (그렇지 않으면 비용이 많이 들 수 있음)

다음으로 테스트 ID( 및 )를 통한 테스트 쿼리 <p>및와 <button> 테스트 쿼리임

에뮬레이트된 DOM에서 원하는 구성 요소를 가져오는 것이 더 쉽기 때문에 테스트 ID를 사용함

7행이 되어서야 실제 통합 테스트가 시작됨

먼저 <p>태그가 비어 있는지 확인함

그런 다음 click이벤트를 시뮬레이션하여 버튼을 클릭함

마지막으로 <p>태그에 "Hello human!"이 포함되어 있는지 확인함

우리가 테스트하는 것은 버튼을 클릭한 후 빈 단락에 텍스트가 포함된다는 것임

물론 누군가 자신의 이름을 입력하는 input을 추가하고 인사말 기능에서 해당 input을 사용할 수 있음

통합 테스트를 실행할 때 터미널에는 아래와 같이 표시됨

<Greeting /> 요소는 단추를 클릭할 때 올바른 인사말을 표시함

종단간(E2E) 테스트

  • 레벨: 높음
  • 범위: 수행할 작업 및 예상 결과에 대한 지침을 제공하여 실제 브라우저에서 사용자 상호 작용을 테스트
  • 가능한 도구: Cypress, Puppeteer

E2E 테스트는 이 목록에서 가장 높은 수준의 테스트임

E2E 테스트는 사람들이 애플리케이션을 보는 방식과 애플리케이션과 상호 작용하는 방식에만 관심이 있음

그들은 코드와 구현에 대해 아무것도 모름

E2E 테스트는 브라우저에 수행할 작업, 클릭할 작업 및 입력할 내용을 알려줌

최종 사용자가 경험할 때 다양한 기능과 흐름을 테스트하는 모든 종류의 상호 작용을 만들 수 있음

문자 그대로 모든 것이 작동하는지 확인하기 위해 응용 프로그램을 클릭하기 위해 상호 작용하는 로봇임

E2E 테스트는 일종의 통합 테스트와 유사하지만, E2E 테스트는 실제 DOM이 있는 실제 브라우저에서 실행됨

우리는 일반적으로 이러한 테스트에서 실제 데이터와 실제 API로 작업함

단위 테스트와 통합 테스트를 모두 포함하는 것이 좋음

그러나 사용자는 브라우저에서 애플리케이션을 실행할 때 예기치 않은 동작에 직면할 수 있고 E2E 테스트는 이에 대한 완벽한 솔루션임

매우 인기 있는 테스트 라이브러리인 Cypress 를 사용하는 예를 살펴보자

다시 말하지만, 우리는 애플리케이션의 코드를 볼 필요가 없음

우리가 가정하고 있는 것은 우리에게 어떤 응용 프로그램이 있고 그것을 사용자로서 테스트하고 싶고 클릭할 버튼과 해당 버튼의 ID를 알고 있다는 것임

describe('Greetings functionality', () => {
  it('should navigate to greetings page and confirm it works', () => {
    cy.visit('<http://localhost:3000>')
    cy.get('#greeting-nav-button').click()
    cy.get('#greetings-input').type('Evgeny', { delay: 400 })
    cy.get('#greetings-show-button').click()
    cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')
  })
})

이 E2E 테스트는 이전 통합 테스트와 매우 유사함

명령은 매우 유사하며 주요 차이점은 실제 브라우저에서 실행된다는 것임

먼저 cy.visit는 애플리케이션이 있는 특정 URL로 이동하는 데 사용함

cy.visit('<http://localhost:3000>')

둘째, cy.get로 ID로 탐색 버튼을 가져온 다음 테스트에 클릭하도록 지시함

해당 작업은 <Greetings/>구성 요소가 있는 페이지로 이동함

실제로 개인 웹사이트에 구성 요소를 추가하고 자체 URL 경로를 제공했음

cy.get('#greeting-nav-button').click()

그런 다음 순차적으로 텍스트 입력을 받고 "Evgeny"를 입력하고 #greetings-show-button버튼을 클릭하고 마지막으로 원하는 인사말 출력을 얻었는지 확인함

cy.get('#greetings-input').type('Evgeny', { delay: 400 })
cy.get('#greetings-show-button').click()
cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')

무슨 일이 일어나고 있는지 볼 수 있도록 테스트 속도를 약간 늦췄지만, 이 모든 것은 일반적으로 매우 빠르게 발생함

터미널 출력은 다음과 같음

접근성 테스트

웹 접근성이란 웹 사이트, 도구 및 기술이 장애가 있는 사람들이 사용할 수 있도록 설계 및 개발되었음을 의미함 - W3C

접근성 테스트는 장애가 있는 사용자가 웹 사이트에 효과적으로 액세스하고 사용할 수 있는지 확인함

이 테스트는 접근성을 염두에 두고 웹 사이트를 구축하기 위한 표준을 준수하는지 확인함

예를 들어, 많은 시력이 없는 사람들은 스크린 리더를 사용함

스크린 리더는 웹사이트를 스캔하여 장애가 있는 사용자가 이해할 수 있는 형식(일반적으로 음성)으로 제공하려고 시도함

개발자는 접근성 테스트를 통해 스크린 리더의 작업을 쉽게 만들고 어디서부터 시작해야 하는지 이해하는 데 도움이 됨

다양한 도구 가 있으며 그 중 일부는 자동화되어 있고 일부는 접근성을 확인하기 위해 수동으로 실행됨

예를 들어 Chrome에는 이미 DevTools에 바로 내장된 도구가 하나 있고 그것은 Lighthouse 임

Lighthouse를 사용하여 E2E 테스트 섹션에서 만든 애플리케이션을 검증해 보자!

Chrome DevTools에서 Lighthouse를 열고 "접근성" 테스트 옵션을 클릭한 다음 보고서를 "생성"함

그게 문자 그대로 우리가 해야 할 전부임

Lighthouse는 작업을 수행한 다음 점수, 실행된 감사 요약 및 점수를 개선할 수 있는 기회 개요가 포함된 보고서를 생성함

그러나 이것은 특정 렌즈에서 접근성을 측정하는 하나의 도구일 뿐임

우리는 모든 종류의 접근성 도구 를 보유하고 있으며, 무엇을 테스트할지, 이러한 지점을 달성하는 데 사용할 수 있는 도구에 대한 계획을 세우는 것이 좋음

시각적 회귀 테스트

  • 레벨: 높음
  • 범위: 코드 변경으로 인한 시각적 차이를 포함하여 애플리케이션의 시각적 구조를 테스트함
  • 가능한 도구: Cypress,Percy,Applitools

때때로 E2E 테스트는 애플리케이션의 마지막 변경 사항이 인터페이스의 시각적 외관을 손상시키지 않았는지 확인하는 데 불충분함

대부분의 경우 코드베이스를 변경하면 앱의 시각적 구조 또는 레이아웃이 손상됨

해결책은 시각적 회귀 테스트임

작동 방식은 매우 간단함

시각적 테스트는 단순히 페이지 또는 구성 요소의 스크린샷을 찍어 이전의 성공적인 테스트에서 캡처한 스크린샷과 비교하고 이러한 테스트에서 스크린샷 간에 불일치가 발견되면 일종의 알림을 제공함

Percy 라는 시각적 회귀 도구를 사용하여 시각적 회귀 테스트가 어떻게 작동하는지 살펴보자!

시각적 회귀 테스트를 수행하는 다른 많은 방법이 있지만 Percy는 실제로 보여주기가 간단함

CSS-Tricks에서 Paul Ryan의 Percy 심층 분석으로 바로 이동할 수 있지만, 개념을 설명하기 위해 훨씬 더 간단한 작업을 수행해보자!

버튼을 입력 하단으로 이동하여 의도적으로 Greeting 애플리케이션의 레이아웃을 깨뜨렸을 때, Percy와 함께 이 오류를 잡아보자!

Percy는 Cypress와 잘 작동하므로 설치 가이드 를 따르고 기존 E2E 테스트와 함께 Percy 회귀 테스트를 실행할 수 있음

describe('Greetings functionality', () => {
  it('should navigate to greetings page and confirm everything is there', () => {
    cy.visit('<http://localhost:3000>')
    cy.get('#greeting-nav-button').click()
    cy.get('#greetings-input').type('Evgeny', { delay: 400 })
    cy.get('#greetings-show-button').click()
    cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')// Percy test
     cy.percySnapshot()// HIGHLIGHT})
})

E2E 테스트 마지막에 추가한 것은 한 줄짜리임: cy.percySnapshot(). 이렇게 하면 스크린샷이 찍혀 비교를 위해 Percy에게 전송되고 그게 끝임

테스트가 끝나면 회귀를 확인할 수 있는 링크를 받게 됨

다음은 터미널에서 나타나는 것임

E2E 테스트도 통과한 것을 볼 수 있는데, 이는 E2E 테스트가 항상 시각적 오류를 포착하지 못하는 방법을 보여줌

Percy로부터 얻은 정보는 다음과 같음

분명히 변경된 사항이 있으며 수정해야 함

성능 시험

성능 테스트는 애플리케이션의 속도를 확인하는 데 유용함

비즈니스에 성능이 중요하고 Core Web Vitals 및 SEO에 중점을 둔  경우, 코드베이스의 변경 사항이 애플리케이션 속도에 부정적인 영향을 미치는지 확실히 알고 싶을 것임

이를 테스트 흐름의 나머지 부분에 적용하거나 수동으로 실행할 수 있음

이러한 테스트를 실행하는 방법과 실행 빈도는 전적으로 개발자에게 달려있음

일부 개발자는 "성능 예산" 을 만들고 앱 크기를 계산하는 테스트를 실행함

테스트에 실패하면 크기가 특정 임계값을 초과하는 경우 배포가 발생하지 않음

또는 성능 메트릭도 측정하므로 Lighthouse를 사용하여 수시로 수동으로 테스트하는 것을 추천함

또는 두 가지를 결합하여 Lighthouse를 테스트 도구 모음으로 빌드 할 수 있음

성능 테스트는 성능과 관련된 모든 것을 측정할 수 있음

애플리케이션이 얼마나 빨리 로드되는지, 초기 번들의 크기, 심지어 특정 기능의 속도까지 측정할 수 있음

성능 테스트는 다소 광범위하고 광활한 환경임

다음은 Lighthouse를 사용한 간단한 테스트를 해보자!

Core Web Vitals에 중점을 두고 설치나 구성 없이 Chrome의 DevTools에서 쉽게 액세스할 수 있기 때문에 확인이 용이함

 

부탁드리는 사항

혹시 잘못된 내용이나, 인용/차용 등에 있어 문제의 소지가 되는 내용이 있다면 언제든 알려주시면 큰 도움이 될 것 같습니다!

긴 글 읽어주셔서 감사합니다 :)

 

출처:
프론트엔드 테스트 정의 전반
https://www.guru99.com/frontend-testing.html
https://www.netguru.com/blog/front-end-testing
https://www.testim.io/blog/front-end-testing-complete-overview/
예시
https://css-tricks.com/front-end-testing-is-for-everyone/
https://lumiloves.github.io/2018/08/21/my-first-frontend-test-code-experience
테스트의 필요성
https://www.perfecto.io/blog/comprehensive-guide-front-end-testing
모범사례
https://meticulous.ai/blog/javascript-ui-testing-best-practices/
테스트 도구
https://www.rainforestqa.com/blog/automated-front-end-testing

 

Memoization?

1. 정의?

  • Memoization이란 컴퓨터 프로그램이 동일한 계산을 반복해야 할 때, 이전에 계산한 값을 메모리에 저장함으로써 동일한 계산의 반복 수행을 제거하여 프로그램 실행 속도를 빠르게 하는 기술
  • useMemo, useCallback, React.memo는 모두 이 Memoization을 기반으로 작동
  • 비용이 많이 드는 함수 호출의 결과를 저장하고 동일한 입력이 다시 발생할 때 캐시된 결과를 반환하여 컴퓨터 프로그램 속도를 높이는 데 주로 사용되는 최적화 기술
  • 소프트웨어 시스템의 일부 측면을 보다 효율적으로 작동시키거나 더 적은 리소스를 사용하도록 수정하는 프로세스

2. 최적화와 메모이제이션

  • 구성 요소의 수명 주기에서 React는 업데이트가 이루어질 때 구성 요소를 다시 렌더링함
    • 웹 페이지 하나가 만들어질 때는 위와 같이, DOM Tree의 구성, 레이아웃 잡기, 페인팅하기 등의 다양한 작업이 이루어짐
    • 리랜더링 시에, 레이아웃 및 페인팅 과정을 또 계산해야 할 수 있음
    ⇒ 그래서 React의 성능을 점검할 때는 컴포넌트 자체의 리랜더링이 불필요하게 반복되고 있지 않은지, 그리고 내부 로직이 쓸데없이 다시 만들어지거나 복잡한 계산을 반복하고 있지는 않은지에 대한 검토가 필요함
  • React가 구성 요소의 변경 사항을 확인할 때 JavaScript가 동등성 및 얕은 비교(equality and shallow comparisons)를 처리하는 방식으로 인해 의도하지 않거나 예기치 않은 변경 사항을 감지할 수 있고 React 애플리케이션은 이러한 변경으로 인해 불필요하게 재렌더링될 수 있음

⇒ 비용이 많이 드는 작업은 시간, 메모리 또는 처리 비용이 많이 들 수 있어 성능저하가 발생할 수 있으므로 사용자 경험 또한 저하될 수 있음

⇒ React는 이를 개선하기 위해 메모 아이디어를 발표함

⇒ 쓸데없이 같은 계산을 반복하게 하지 않게 할 수 있는 방법은? 결과를 기억하는 것

useMemo

1. 이건 뭐야?

정의?

  • 이전 값을 기억해두었다가 조건에 따라 재활용하여 성능을 최적화 하는 용도로 사용됨 (특정 value를 재사용)
  • useMemo는 함수의 결과 값을 memoized하여 불필요한 연산을 관리
  • 사용 형식
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • useMemo의 특징은 일단 함수 호출 이후의 return 값이 memoized되며, 두 번째 파라미터인 배열의 요소가 변경될 때마다 첫 번째 파라미터의 callback 함수를 다시 생성하는 방식임

useRef와의 차이

useMemo는 deps가 변경되기 전까지 값을 기억하고, 실행후 값을 보관하는 역할로도 사용

  • useMemo는 복잡한 함수의 return 값을 기억한다는 점에서 값만 기억하는 useRef와는 다름
  • useRef는 특정 값을 기억하는 경우, useMemo는 복잡한 함수의 return값을 기억하는 경우에 사용됨

동작방식

  • 초기 렌더링 중에 useMemo(compute,dependencies)계산을 호출하고 계산 결과를 메모한 다음 구성 요소로 반환함
  • useMemo종속성 중 하나가 변경된 경우에만 메모된 값을 다시 계산하며, 이 최적화는 모든 렌더링에서 비용이 많이 드는 계산을 피하는 데 도움이 됨
  • 다음 렌더링 중에 종속성이 변경되지 않으면 useMemo() 는 컴퓨팅을 호출하지 않고 메모된 값을 반환함

2. 어디에 써?

  • 비용이 많이 드는 계산을 메모화하는 데 사용
    • 여기서 비싸다는 의미는 메모리와 같은 리소스를 많이 사용한다는 것을 의미

3. 주의점은?

종속성 비교로 인한 계산 비용

  • 내부적으로 React의 useMemo Hook은 값을 다시 계산해야 하는지 여부를 결정하기 위해 다시 렌더링할 때마다 종속성 배열의 종속성을 비교해야 하며, 종종 이 비교를 위한 계산은 단순히 값을 다시 계산하는 것보다 비용이 더 많이 들 수 있음 ⇒ useMemo애플리케이션에서 너무 자주 구현 하면 성능이 저하될 수 있음
  • 프로파일링 도구를 사용하여 비용이 많이 드는 성능 문제를 식별할 수 있음

4. 예시를 살펴보자!

useMemo 사용 전

import { useState } from 'react';

export function MyComponent() {
  const [number, setNumber] = useState(1);
  const [inc, setInc] = useState(0);

  const factorialResult = calculateFactorial(number);
  const onChange = event => {
    setNumber(Number(event.target.value));
  };
  const onClick = () => setInc(i => i + 1);

  return (
    <div>
      Factorial of the following Number
      <input type="number" value={number} onChange={onChange} />
      is {factorialResult}
      <button onClick={onClick}>Increment</button> <span>{inc}</span>
    </div>
  );
}

function calculateFactorial(number) {
  console.log('calculateFactorial called!');
  return number <= 0 ? 1 : number * calculateFactorial(number - 1);
}
  1. 입력 값을 변경할 때마다 factorialResult가 계산 'calculateFactorial(number) called!'되어 콘솔에 기록됨
  2. Increment 버튼을 클릭할 때마다 inc상태 값이 업데이트됩니다. 상태 값을 업데이트 하면 다시 렌더링 inc이 트리거됨
  3. <MyComponent /> 가 2.번의 이벤트로 인해, 재렌더링되는 동안 다시 calculateFactorial계산 되어 'calculateFactorial(n) called!'값이 콘솔에 기록됨

⇒ useMemo(()=> calculateFactorial(number), [number]) 으로 React는 계산값을 메모할 수 있음

useMemo 사용 후

import { useState, useMemo } from 'react';

export function MyComponent() {
  const [number, setNumber] = useState(1);
  const [inc, setInc] = useState(0);

  const factorialResult = useMemo(() => calculateFactorial(number) , [number]);
  const onChange = event => {
    setNumber(Number(event.target.value));
  };
  const onClick = () => setInc(i => i + 1);

  return (
    <div>
      Factorial of the following Number
      <input type="number" value={number} onChange={onChange} />
      is {factorialResult}
      <button onClick={onClick}>Increment</button> <span>{inc}</span>
    </div>
  );
}

function calculateFactorial(number) {
  console.log('calculateFactorial called!');
  return number <= 0 ? 1 : number * calculateFactorial(number - 1);
}
  • 입력(input) 값을 변경할 때마다 'calculateFactorial(n) called!'가 콘솔에 기록되지만, Increment 버튼을 클릭 하면 useMemo 에 의해, 메모된 계산값이 반환되기 때문에, 'calculateFactorial(n) called!' 은 콘솔에 기록되지 않음

useCallback

1. 이건 뭔데?

  • useCallback은 리액트의 렌더링 성능을 위해서 제공되는 Hook이다.
  • 부모컴포넌트에서 자식컴포넌트에 prop으로 넘겨주는 함수가 있을 때, 부모 컴포넌트가 렌더링 될 때마다 내부적으로 사용된 함수도 새로 생성되어, 자식 컴포넌트에 Prop으로 새로 생성된 함수가 넘겨지게 되면 불필요한 리렌더링이 일어날 수 있다.
  • ⇒ 이 경우, 함수를 memoized하여 해결할 수 있음
  • 메모리제이션된 함수를 반환하는 것이 핵심
  • 사용형식
  • const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
  • useCallback을 사용하여 함수를 memoized 시키면, 종속성 배열의 종속성이 변경되는 경우에만 이 함수가 다시 정의됨
  • useCallback의 특징
  1. useCallback은 function의 메모리 재할당을 막기위한 수단
  2. 여러곳에서 사용되는 컴포넌트가 불필요하게 같은 function을 메모리에 여러번 할당한다면, useCallback을 사용한 최적화가 필요
  3. useCallback은 함수의 결과를 메모리에 저장하는게 아니라, 메모리에 저장된 함수를 같은 컴포넌트들에서 공유하는 개념

2. 언제 써?

  • 함수를 메모하기 위해 사용되며, 부모 구성 요소를 다시 렌더링할 때마다 함수가 다시 초기화되는 것에 대해 걱정하지 않고 다른 구성 요소에 함수를 전달할 때 이미 약간의 성능 향상이 있음
  • useCallback은 React.Memo와 함께 사용할 때 특히 유용함
  • 컴포넌트가 랜더링 될 때마다 내부에 선언되어 있던 표현식이 다시 선언되어 사용됨. 이 때, 컴포넌트 내부에 있는 함수는 변동이 없음에도 컴포넌트가 리랜더링 될 때마다 다시 선언됨. ⇒ 이런 경우에 useCallback을 import해서 사용하던 함수의 실행문을 넣어주면 랜더링 될 때마다 선언되는 것을 피할 수 있고 의존성 배열에 요소를 추가하면 해당 값이 변경될 때 재선언 가능.
  • 또한, 상위컴포넌트의 함수가 매번 재선언되면, 내용이 같다고 하더라도 하위컴포넌트는 넘겨받는 함수가 달라졌다고 인식함. ⇒ 따라서 하위컴포넌트가 React.memo() 등으로 최적화 되어있고, 그 하위 컴포넌트에게 callback 함수를 props로 넘길 경우에, 상위컴포넌트에서 useCallback으로 선언하는 것이 최적화에 도움됨.
    • React.memo()로 함수형 컴포넌트 자체를 감싸면 넘겨 받는 props가 변경되지 않았을 때는 상위 컴포넌트가 메모리제이션된 함수형 컴포넌트(이전에 렌더링된 결과)를 사용하게 됨.

3. 주의점은?

4. 사용 예시를 살펴보자

React.memo로 래핑된 컴포넌트가 callback을 받을 때

const MemoisedItem = React.memo(Item);
const List = () => {
**// this HAS TO be memoised, otherwise `React.memo` for the Item is useless**
  const onClick = () => {console.log('click!')};
  return <MemoisedItem onClick={onClick} country="Austria" />
}

  • 함수 객체는 "일반" 객체와 동일한 비교 원칙을 따름 함수 객체는 오직 자신에게만 동일
  • 몇가지 함수를 비교해보자.
function sumFactory() {
  return (a, b) => a + b;
}

const sum1 = sumFactory();
const sum2 = sumFactory();

console.log(sum1 === sum2); // => false
console.log(sum1 === sum1); // => true
console.log(sum2 === sum2); // => true
  • sumFactory()는 팩토리 함수이다. 이 함수는 2가지 숫자를 더해주는 화살표 함수를 반환
  • 함수 sum1과 sum2는 팩토리에 의해 생성된 함수이고, 두 함수 모두 두 숫자를 더해주는 함수임. 그러나 sum1과 sum2는 다른 함수 객체임.
  • 부모 컴퍼넌트가 자식 컴퍼넌트의 콜백 함수를 정의한다면, 새 함수가 암시적으로 생성될 수 있음.
  • 예시를 통해 알아보자.
  • Logout 컴퍼넌트는 콜백 prop인 onLogout을 갖는다.
function Logout({ username, onLogout }) {
  return <div onClick={onLogout}>Logout {username}</div>;
}

const MemoizedLogout = React.memo(Logout);
  • 함수의 동등성이란 함정 때문에, 메모이제이션을 적용할 때는 콜백을 받는 컴퍼넌트 관리에 주의해야함.
  • 리렌더를 할 때 마다 부모 함수가 다른 콜백 함수의 인스턴스를 넘길 가능성이 있음.
function MyApp({ store, cookies }) {
  return (
    <div className="main">
      <header>
        <MemoizedLogout
          username={store.username}
          onLogout={() => cookies.clear()}
        />
      </header>
      {store.content}
    </div>
  );
}
  • 동일한 username 값이 전달되더라고, MemoizedLogout은 새로운 onLogout 콜백 때문에 리렌더링을 하게 됨.
  • ⇒ 메모이제이션이 중단되게 되는 것
  • 이 문제를 해결하려면 onLogout prop의 값을 매번 동일한 콜백 인스턴스로 설정해야만 함. useCallback()을 이용해서 콜백 인스턴스를 보존시킬 수 있음.
const MemoizedLogout = React.memo(Logout);

function MyApp({ store, cookies }) {
  const onLogout = useCallback(() => {
    cookies.clear();
  }, []);
  return (
    <div className="main">
      <header>
        <MemoizedLogout username={store.username} onLogout={onLogout} />
      </header>
      {store.content}
    </div>
  );
}
  • useCallback(() => { cookies.clear() }, []) 는 항상 같은 함수 인스턴스를 반환하고,  MemoizedLogout의 메모이제이션이 정상적으로 동작하도록 수정되었음

컴포넌트가 hooks(useMemo, useCallback or useEffect)에 dependency로 callback을 받을 때

const Item = ({ onClick }) => {
  useEffect(() => {
// some heavy calculation here
    const data = ...
    onClick(data);
**// if onClick is not memoised, this will be triggered on every single render**
  }, [onClick])
  return <div>something</div>
}
const List = () => {
// this HAS TO be memoised, otherwise `useEffect` in Item above
// will be triggered on every single re-render
  const onClick = () => {console.log('click!')};
  return <Item onClick={onClick} country="Austria" />
}

나쁜 사용 사례

import { useCallback } from 'react';
function MyComponent() {
  // Contrived use of `useCallback()`
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);
  return <MyChild onClick={handleClick} />;
}
function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

  • 첫 번째 문제는 렌더링 useCallback()할 때마다 후크가 호출 된다는 것 : 그것은 이미 렌더링 성능을 감소시킴
  • 두 번째 문제는 사용 useCallback()이 코드 복잡성을 증가시키는 것 : useCallback(..., deps) 의 deps와 memoized 콜백 내에서 사용 중인 것과 동기화 deps를 유지해야 함.
  • useCallback()의미가 있을까? : <MyChild>구성 요소가 가볍고 다시 렌더링해도 성능 문제가 발생하지 않기 때문일 가능성이 높음

⇒ 결론적으로 최적화를 하지 않는 것보다 최적화 비용이 더 많이 듬

React.memo

1. 이게 뭔데?

  • UI 성능을 증가시키기 위해, React는 고차 컴포넌트(Higher Order Component, HOC) React.memo()를 제공
    • 고차 컴포넌트
      • 고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술
      • 고차 컴포넌트(HOC)는 React API의 일부가 아니며, React의 구성적 특성에서 나오는 패턴
      • 구체적으로, 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수
  • 렌더링 결과를 메모이징(Memoizing)함으로써, 불필요한 리렌더링을 건너뜀
  • 컴포넌트가 동일한 props로 동일한 결과를 렌더링해낸다면, React.memo를 호출하고 결과를 메모이징(Memoizing)하도록 래핑하여 경우에 따라 성능을 향상시킬 수 있음 ⇒ React.memo는 컴포넌트를 렌더링하지 않고 마지막으로 렌더링된 결과를 재사용함
  • React.memo는 props 변화에만 영향을 주며, React.memo로 감싸진 함수 컴포넌트 구현에 useState, useReducer 또는 useContext 훅을 사용한다면, 여전히 state나 context가 변할 때 다시 렌더링됨
    • 사용형태
    const MyComponent = React.memo(function MyComponent(props) {
      /* props를 사용하여 렌더링 */
    });
    
    • (간단한 예) React.memo는 일반적으로 아래와 같이 사용됨
      • React.memo는 Welcome의 결과를 Memoization해서 이후 props가 변경될때까지 현재 memoized된 내용을 그대로 사용하여 리렌더링을 막음
      • ⇒ 이렇게 Memoized된 내용을 재사용하여 렌더링시 가상 DOM에서 달라진 부분을 확인하지 않아 성능상의 이점이 생기게 됨
    const Welcome = ({ name }) => {
      return <h1>Hello { name }</h1>;
    };
    
    export default React.memo(Welcome);
    
  • props가 갖는 복잡한 객체에 대하여 얕은 비교만을 수행하는 것이 기본 동작이며, 다른 비교 동작을 원한다면, 두 번째 인자로 별도의 비교 함수를 제공하면 됨.
    • 얕은 비교 : 원시 값의 경우는 같은 값을 갖는지 확인하고 객체나 배열과 같은 참조 값은 같은 주소 값을 갖고 있는지 확인
function MyComponent(props) {
  /* props를 사용하여 렌더링 */
}
function areEqual(prevProps, nextProps) {
  /*
  nextProps가 prevProps와 동일한 값을 가지면 true를 반환하고, 그렇지 않다면 false를 반환
  */
}
export default React.memo(MyComponent, areEqual);
  • (간단한 예) Movie의 props가 동일한지 수동으로 비교해보자.
    • moviePropsAreEqual() 함수는 이전 props와 현재 props가 같다면 true를 반환할 것
function moviePropsAreEqual(prevMovie, nextMovie) {
  return (
    prevMovie.title === nextMovie.title &&
    prevMovie.releaseDate === nextMovie.releaseDate
  );
}

const MemoizedMovie2 = React.memo(Movie, moviePropsAreEqual);

2. 어디에 쓸까?

사용처

  1. Pure Functional Component(동일한 상태 및 props에 대해 동일한 출력을 렌더링하는 컴포넌트)에서 Rendering이 자주일어날 경우
  2. re-rendering이 되는 동안에도 계속 같은 props값이 전달될 경우
  3. UI element의 양이 많은 컴포넌트의 경우
  4. 동일한 props에 항상 같은 것을 랜더링하는데, 같은 props로 랜더링이 자주 일어날 때(즉, 일부 데이터를 가져오기 위해 네트워크 호출을 해야 하고 데이터가 동일하지 않을 가능성이 있는 경우 사용 지양)

결론

  • 컴퍼넌트가 무겁고 비용이 큰 연산이 있는데 같은 props로 자주 렌더링되거나 같은 props로 리랜더링이 엄청 자주 일어난다면  React.memo()로 컴퍼넌트를 래핑하면 좋음
    (단, 동일한 props로는 항상 같은 리랜더링 결과가 나올 때)

3. 주의점은?

일반적인 주의사항

  • 이 메서드는 오직 **성능 최적화**를 위하여 사용되므로 렌더링을 “방지”하기 위하여 사용할 경우, 버그가 생성될 수 있음
  • 최적화를 위한 연산이 불필요한 경우엔 비용만 발생시키기 때문에 무조건적인 사용은 지양
  • React.memo는 Props의 변경 사항만 확인하므로, React.memo에 래핑된 함수 컴포넌트 요소에 useState , useReducer 또는 useContext Hook이 있는 경우 상태 또는 컨텍스트가 변경될 때 여전히 다시 렌더링됨

부모가 전달하는 callback 함수에 대한 주의사항

  • useCallback과 함께 사용할 때의 예제

useCallback 사용 전

function CountButton({ onClick, count }) {
  return <button onClick={onClick}>{count}</button>;
}
function DualCounter() {
  const [count1, setCount1] = React.useState(0);
  const increment1 = () => setCount1(c => c + 1);
  const [count2, setCount2] = React.useState(0);
  const increment2 = () => setCount2(c => c + 1);
  return (
    <>
      <CountButton count={count1} onClick={increment1} /> // React.memo로 래핑되었다는 가정
      <CountButton count={count2} onClick={increment2} /> // React.memo로 래핑되었다는 가정
    </>
  );
}

useCallback 사용 후

const CountButton = React.memo(function CountButton({ onClick, count }) {
  return <button onClick={onClick}>{count}</button>;
});
function DualCounter() {
  const [count1, setCount1] = React.useState(0);
  const increment1 = React.useCallback(() => setCount1(c => c + 1), []);
  const [count2, setCount2] = React.useState(0);
  const increment2 = React.useCallback(() => setCount2(c => c + 1), []);
  return (
    <>
      <CountButton count={count1} onClick={increment1} /> // React.memo로 래핑되었다는 가정
      <CountButton count={count2} onClick={increment2} /> // React.memo로 래핑되었다는 가정
    </>
  );
}
  • state count1이 변경되었을 때, state 변경이 없었던 count2를 참조하는 CountButton 컴포넌트는 리렌더리 되지 않아야 함(React.memo로 래핑되었다는 가정)
  • 만약 increment2 함수에 useCallback이 없었다면, DualCounter 컴포넌트는 state의 변경으로 인해 re-rendering 될 것이고, increment1과 increment2 함수 모두 새로 생성되어 2개의 CountButton 컴포넌트는 모두 re-rendering 될 것
  • 하지만 increment1, increment2 함수에 useCallback을 사용함으로써 두개의 함수는 재 생성이 되지 않고 (종속배열도 비어있음) 변경된 count1을 참조하는 CountButton만 re-rendering 되게 됨

4. 예시로 알아보자!

  • 같은 props로 렌더링이 자주 일어나는 컴퍼넌트에 사용하기 좋음
    • React.memo()를 사용하기 가장 좋은 케이스는 함수형 컴퍼넌트가 같은 props로 자주 렌더링 될거라 예상될 때이다.
    • 일반적으로 부모 컴퍼넌트에 의해 하위 컴퍼넌트가 같은 props로 리렌더링 될 때가 있음
  • Movie의 부모 컴퍼넌트인 실시간으로 업데이트되는 영화 조회수를 나타내는 MovieViewsRealtime 컴퍼넌트가 있다고 하자.
function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <Movie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}
  • 이 어플리케이션은 주기적(매초)으로 서버에서 데이터를 폴링(Polling)해서 MovieViewsRealtime 컴퍼넌트의 views를 업데이트함
// Initial render
<MovieViewsRealtime views={0} title="Forrest Gump" releaseDate="June 23, 1994"/>// After 1 second, views is 10
<MovieViewsRealtime views={10} title="Forrest Gump" releaseDate="June 23, 1994"/>// After 2 seconds, views is 25
<MovieViewsRealtime views={25} title="Forrest Gump" releaseDate="June 23, 1994"/>// etc
  • views가 새로운 숫자가 업데이트 될 때 마다 MoviewViewsRealtime 컴퍼넌트 또한 리렌더링 되며, Movie 컴퍼넌트 또한 title이나 releaseData가 같음에도 불구하고 리렌더링 됨
  • 이때가 Movie 컴퍼넌트에 메모이제이션을 적용할 적절한 케이스임
  • MovieViewsRealtime에 메모이징된 컴퍼넌트인 MemoizedMovie를 대신 사용해 성능을 향상해보자.
function MovieViewsRealtime({ title, releaseDate, views }) {
  return (
    <div>
      <MemoizedMovie title={title} releaseDate={releaseDate} />
      Movie views: {views}
    </div>
  );
}
  • title 혹은 releaseDate props가 같다면, React는 MemoizedMovie를 리렌더링 하지 않을 것이다. 이렇게 MovieViewsRealtime 컴퍼넌트의 성능을 향상할 수 있음

React.memo vs. useMemo vs. useCallback

1. 공통점

공통점

  • React.memo, useMemo, useCallback은 모두 불필요한 렌더링 또는 연산을 제어하는 용도로 성능 최적화에 그 목적이 있음
  • 재렌더링 사이의 메모이제이션임
  • 전달하려는 항목이 새로운 참조여도 상관없다면, 사용하지 말아야 한다. 매번 새로운 참조여도 상관없는데, 새로운 참조라면 메모이제이션하는 것이 의미가 없음

useMemo와 useCallback을 사용해야 하는 경우

  1. 하위트리에 많은 Consumer가 있는 값을 Context Provider에 전달해야 하는 경우 useMemo를 사용하는 것이 좋음  <ProductContext.Provider value={{id, name}} >의 경우, 어떤 이유로든 해당 컴포넌트가 리렌더링 된다면 id  name이 동일하더라도 매번 새로운 참조를 만들어 죄다 리렌더링 될 것
  2. 계산 비용이 많이 들고, 사용자의 입력 값이 렌더링 이후로도 참조적으로 동일할 가능성이 높은 경우, useMemo를 사용하는 것이 좋음
  3. 매우 큰 리액트 트리 구조 내에서, 부모가 리렌더링 되었을 때 이에 다른 렌더링 전파를 막고 싶을 때 사용하자. 자식 컴포넌트가 React.memo  React.PureComponent일 경우, 메모이제이션된 props를 사용하게되면 딱 필요한 부분만 리렌더링 될 것

사용팁

React DevTools Profiler를 사용하면 컴포넌트의 리렌더링 속도가 느린 경우, 상태 변경이 일어났을 때 얼마나 렌더링 시간이 걸렸는지 조사할 수 있음

이렇게 하면 거대한 계단식 리렌더링을 방지하기 위해 React.memo를 사용할 위치를 찾을 수 있고, 필요한 경우 useCallback useMemo를 사용하여 상태변경을 더 효율적으로 만들 수 있음

2. 차이점

  • React.memo는 HOC이고, useMemo와 useCallback은 hook
  • React.memo는 HOC이기 때문에 클래스형 컴포넌트, 함수형 컴포넌트 모두 사용 가능하지만, useMemo는 hook이기 때문에 함수형 컴포넌트 안에서만 사용 가능
  • useMemo는 함수의 연산량이 많을때 이전 결과값을 재사용하는 목적이고, useCallback은 함수가 재생성 되는것을 방지하기 위한 목적(React.memo와 useMemo의 차이는 어디에 활용되는가임)
  • React.memo의 경우에는 컴포넌트를 받아 컴포넌트를 반환한다.
  • useMemo의 경우에는 값을 계산하는 과정을 최적화해 값을 반환받음(컴포넌트도 값이기에 useMemo 안에 넣을 수 있음)

3. 주의점

  • 일부 개발자가 흔히 저지르는 실수는 성능 문제를 방지하기 위해 필요하지 않은 경우에도 이러한 후크(및 기타 최적화 기술)를 사용하는 것임
  • 이는 코드를 더 복잡하게 만들고(따라서 유지 관리하기 더 어렵게 만들고) 경우에 따라 성능이 더 나빠지기 때문에 권장되지 않음
  • 성능 문제를 찾은 후 이러한 기술을 적용해야 함
  • 원하는 만큼 빠르게 실행되지 않는 경우 병목 현상이 있는 부분을 조사하고 해당 부분을 최적화가 필요
  • useCallback을 사용하여 접근하는 좋은 방법은 능동적이기보다는 반응적으로 접근하는 것임
  • 즉, 구성 요소에 따라 조급한 성능 최적화가 아니라 분명히 필요할 때 사용하는 것이 중요함
  • useCallback의 함수 본문 내부에 있는 모든 함수를 래핑하지 않도록 하자.

React Devtools Profile

React Devtools로 프로파일링

  1. 먼저 React Devtools 브라우저 확장 프로그램을 다운로드해야 함
    1. 현재 Chrome 및 Firefox 에서 사용할 수 있음
    2. 여기서는 Chrome을 사용한다고 가정하지만, 방법은 크게 다르지 않음

  1. "Profiler" 탭을 선택
  2. 작은 기어 아이콘을 클릭하고 "프로파일링 중 각 구성 요소가 렌더링된 이유 기록" 옵션을 활성화함
  • 일반적인 흐름은 다음과 같음
    1. 작은 파란색 "녹음" 원을 눌러 녹음 시작
    2. 애플리케이션에서 몇 가지 작업을 수행
    3. 녹음을 중지
    4. 기록된 스냅샷을 보고 무슨 일이 일어났는지 확인 가능
  • 각 렌더링은 별도의 스냅샷으로 캡처되며 화살표를 사용하여 탐색할 수 있음
    (참조) https://storage.googleapis.com/joshwcomeau/devtools-demo-v2.mp4
  • 관심 있는 구성 요소를 클릭하면 특정 구성 요소가 다시 렌더링된 이유를 정확하게 확인할 수 있음.
  • React 프로파일러에는 다시 렌더링하는 구성 요소를 강조 표시할 수 있는 옵션이 있음

 

이 설정을 사용하면 다시 렌더링하는 구성 요소 주위에 녹색 사각형이 깜박이는 것을 볼 수 있고 이를 통해 상태 업데이트가 얼마나 광범위한지 확인할 수 있고 일부 요소가 재렌더링을 성공적으로 피하는지 테스트할 수 있음

 

개인적으로 느낀 점

일전에 React를 사용하며, 부모 Component에서 자식 Component로 callback을 Prop으로 내려줬는데 의도치않게 너무 많은 랜더링이 일어나는 이슈를 겪은 적이 있다.

그 때, useCallback으로 해결한 경험이 있어서 그 이후 useCallback을 남발하게 되었던 것 같다.

메모이제이션이라는 것이 어딘가에 저장을 하는 만큼(메모리) 결코 공짜가 아니라는 생각이 들었고 과연 나는 useCallback을 효율적으로 사용하고 있는가라는 의문이 들었다.

관련하여 찾다보니, React의 다른 메모이제이션 훅, HOC에 대해서도 찾을 수 있었다.

역시, 메모이제이션 기능은 공짜가 아니었고 오히려 이러한 성급한 최적화 시도가 성능을 더 저하시킬 수 있다는 것을 확인하였다.

useCallback의 경우에도 꼭 필요한 경우(React.memo로 래핑한 자식 컴포넌트에 callback을 넘겨주는 경우, 자식 컴포넌트로 내려가는 callback으로 인해, 자식의 useEffect가 의도치 않게 계속 시행되는 경우 등)와 사용에 대한 근거 없이는 사용을 자제해야겠다는 생각을 했다.

이전에 너무 많은 랜더링이 일어난 상황이 현재는 잘 기억나진 않지만, 아마도 useEffect 종속성 문제와 겹치면서 일어난 참사이지 않았을까 생각이 든다.

당시 React.memo를 사용하는 상황은 아니었기 때문에 굳이 useCallback을 쓰지 않고 해결할 수 있는 방법도 있지 않았을까 생각이 들고 상황상 여의치 않다면 이전에 해결한 방법과 동일하게 useCallback을 결국 써야했을 것 같다.

 

부탁드리는 사항

혹시 잘못된 내용이나, 인용/차용 등에 있어 문제의 소지가 되는 내용이 있다면 언제든 알려주시면 큰 도움이 될 것 같습니다!

긴 글 읽어주셔서 감사합니다 :)

 

출처:
메모이제이션 정의
https://ko.wikipedia.org/wiki/메모이제이션
useMemo
https://www.digitalocean.com/community/tutorials/react-usememo
https://ko.reactjs.org/docs/react-api.html React.Memo
https://ui.toast.com/weekly-pick/ko_20190731
Pure Functional Component
https://blog.logrocket.com/what-are-react-pure-functional-components/
전반적 설명 및 예시
https://www.joshwcomeau.com/react/usememo-and-usecallback/
https://medium.com/geekculture/great-confusion-about-react-memoization-methods-react-memo-usememo-usecallback-a10ebdd3a316
전반적 역할 설명 및 비교
https://medium.com/hcleedev/web-최적화와-react-memo-usememo-알아보기-4324a237a039 https://velog.io/@sunkim/React.memo-useMemo-useCallback-역할-및-차이점 https://ssangq.netlify.app/posts/react-memo-useMemo-useCallback
https://www.developerway.com/posts/how-to-use-memo-use-callback
https://ddingg.tistory.com/119
useMemo vs. useEffect
https://stackoverflow.com/questions/56028913/usememo-vs-useeffect-usestate
useMemo와 useCallback 사용처
https://yceffort.kr/2022/04/best-practice-useCallback-useMemo
React Devtools Profile
https://www.joshwcomeau.com/react/why-react-re-renders/
useCallback 사용처와 주의사항
https://www.developerway.com/posts/how-to-write-performant-react-code
https://dmitripavlutin.com/react-usecallback/
useCallback, useMemo를 남용하면 안 되는 이유
https://kentcdodds.com/blog/usememo-and-usecallback
https://nicozerpa.com/when-to-use-usememo-and-usecallback-in-react/
https://amberwilson.co.uk/blog/how-and-when-to-use-react-usecallback/

 

+ Recent posts