문제상황

현대 웹 개발에서 성능 최적화는 선택이 아닌 필수가 되었다. 특히 구글의 Core Web Vitals가 검색 순위 결정 요소로 포함되면서, 웹 성능 지표에 대한 지속적인 모니터링과 개선이 더욱 중요해졌다.

하지만 매번 빌드하고.. 서버 키고… 크롬 개발자도구 들어가서 lighthouse 분석하고… 이 과정에서 성능이 크게 떨어지는 지점이 있으면 핫픽스하고 하는 일련의 과정 자체가 너무 비효율적인 것 같다는 생각이 들어 Lighthouse CI를 통해 릴리즈 직전의 development 브랜치와 메인에서 각각 웹 바이탈 지표를 측정하면서 CI(Continuous Integration) 과정을 보완하려 한다.

해결하기

매번 측정하기 귀찮은 만큼, 처음에 필요한 페이지들을 선제적으로 PR을 올릴 때 Lighthouse CI(lhci)를 사용하여 CI 과정에서도 lighthouse지표를 볼 수 있도록 하기로 했다.

Lighthouse CI란?

Lighthouse CI는 구글에서 개발한 오픈소스 도구로, CI/CD 파이프라인에서 Lighthouse 성능 테스트를 자동화할 수 있게 해준다. 단순히 일회성 성능 측정을 넘어서 지속적인 성능 모니터링과 성능 회귀 방지를 가능하게 한다.

주요 특징

자동화된 성능 측정: 코드 변경사항이 있을 때마다 자동으로 성능 지표를 측정

성능 회귀 감지: 기준값 대비 성능이 저하되었을 때 빌드를 실패시켜 성능 회귀를 방지

성능 회귀(performance regression) 소프트웨어가 여전히 올바르게 작동하지만 이전보다 더 느리게 수행되거나 더 많은 메모리나 자원을 사용하는 상황

다양한 환경 지원: 로컬 개발환경부터 GitHub Actions, Jenkins 등 다양한 CI 환경에서 실행 가능

풍부한 리포팅: 성능 지표를 시각화하고 히스토리를 추적 가능

설정하기

의존성 설치

yarn global add lhcl

yarn add -D lhcl

lighthouserc.js 설정하기

module.exports = {
  ci: {
    collect: {
      startServerCommand: "yarn start",
      url: ["http://localhost:3000"],
      numberOfRuns: 5,
    },
    assert: {
      assertions: {
        "categories:performance": ["warn", { minScore: 0.9 }],
        "categories:accessibility": ["warn", { minScore: 0.9 }],
      },
    },
    upload: {
      target: "filesystem",
      outputDir: "./lhci_reports",
      reportFilenamePattern: "--report.",
    },
  },
};

assertions는 내가 설정한 기준에 맞춰서 기준 이하일 경우 경고해줄 수 있도록 하였다.

upload의 경우는 filesystem을 이용해서 lhci_report로 outputDir를 설정해주면

// 내가 설정한 횟수만큼 lighthouse 측정 결과가 배열에 담겨 나옴
[
  {
    "url": "http://localhost:3000/",
    "isRepresentativeRun": false,
    "htmlPath": "프로젝트 경로/설정한 날짜-report.html",
    "jsonPath": "json 경로",
    "summary": {
      "performance": "성능 지표",
      "accessibility": "접근성 지표",
      "best-practices": "웹 표준 준수 여부",
      "seo": "검색엔진 성능"
    }
  },
]

이런 식으로 내가 설정해놓은 결과를 볼 수 있는데, 이에 따라서 PR의 댓글로 Lighthouse 지표 측정 결과를 띄워줄 수도 있다.

Workflow 설정

name: Lighthouse CI
on:
  pull_request:
    branches: [main, development]
  push:
    branches: [main]
permissions:
  contents: read
  issues: write
  pull-requests: write
 
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
 
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: "18"
          cache: "yarn"
 
      - name: Install dependencies
        run: |
          yarn set version berry
          yarn install
 
      - name: Build Next.js app
        run: yarn build
        env:
          필요한 환경변수 설정
 
      - name: Run Lighthouse CI
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
        run: yarn lhci || echo "Failed to run Lighthouse CI"
      - name: Format lighthouse score
        id: format_lighthouse_score
        uses: actions/github-script@v7 # 최신 버전 사용
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
 
            const fs = require('fs');
 
            // 파일 경로 수정 (실제 경로로 변경 필요)
            const results = JSON.parse(fs.readFileSync("manifest.json이 있는 경로"));
            let comments = "";
 
            results.forEach((result, idx) => {
                const { summary, jsonPath } = result;
                
                // details 변수를 summary 뒤에 선언
                const details = JSON.parse(fs.readFileSync(jsonPath));
                const { audits } = details;
 
                const formatResult = (res) => Math.round(res * 100);
 
                Object.keys(summary).forEach(
                (key) => (summary[key] = formatResult(summary[key]))
                );
 
                // 점수 색상 함수
                const score = (res) => (res >= 90 ? "🟢" : res >= 50 ? "🟠" : "🔴");
 
                const comment = [
                ``,
                `## 🚀 Lighthouse Report for TEST${idx+1}`,
                `### 📅 Date: ${new Date().toLocaleDateString()}`,
                `| Category | Score |`,
                `| --- | --- |`,
                `| ${score(summary.performance)} Performance | ${summary.performance} |`,
                `| ${score(summary.accessibility)} Accessibility | ${summary.accessibility} |`,
                `| ${score(summary['best-practices'])} Best Practices | ${summary['best-practices']} |`,
                `| ${score(summary.seo)} SEO | ${summary.seo} |`
                ].join("\n");
 
                const detail = [
                ``,
                `### 📊 Performance Details`,
                `| Metric | Score | Value |`,
                `| --- | --- | --- |`,
                `| ${score(Math.round(audits["first-contentful-paint"].score * 100))} First Contentful Paint | ${Math.round(audits["first-contentful-paint"].score * 100)} | ${audits["first-contentful-paint"].displayValue} |`,
                `| ${score(Math.round(audits["largest-contentful-paint"].score * 100))} Largest Contentful Paint | ${Math.round(audits["largest-contentful-paint"].score * 100)} | ${audits["largest-contentful-paint"].displayValue} |`,
                `| ${score(Math.round(audits["cumulative-layout-shift"].score * 100))} Cumulative Layout Shift | ${Math.round(audits["cumulative-layout-shift"].score * 100)} | ${audits["cumulative-layout-shift"].displayValue} |`
                ].join("\n");
                
                comments += comment + "\n" + detail + "\n\n";
                }); 
 
            // 출력 설정
                core.setOutput('comments', comments);
 
      - name: Comment PR
        uses: unsplash/comment-on-pr@v1.3.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          msg: ${{ steps.format_lighthouse_score.outputs.comments }}
 

이런 식으로 보고서가 출력되도록 하면 된다! 권한 입력을 안할 경우 PR 댓글 쓰는 과정에서 unsplash가 권한에서 막혀 오류가 나니 권한 설정은 주자

그럼 이런 식으로 결과가 나오는 것을 볼 수 있다!

참조

https://tech.kakaoent.com/front-end/2022/220602-lighthouse-with-github-actions/