어그로성 제목 죄송합니다...
lighthouse 를 통해 최적화를 할 수 있다는 것을 다들 알고 계실겁니다 ㅎㅎ 근데 다들 lighthouse 측정은 어떻게 하시나요?
저는 측정하고자 하는 페이지에 들어가서 개발자 도구에 들어가고 lighthouse 보고서를 생성해서
거기에 따른 점수를 통해서 압축이나, 필요없는 라이브러리 제거, 동적 import 등의 여러가지 해법으로
최적화를 진행했었습니다. 그래서 페이지가 많아져도 클라이언트에게 효율적이고 빠른 페이지를 제공하기 위해서
모든 페이지에 대한 평가를 진행했습니다!
이 글을 보기 전까지는 말이죠….
https://velog.io/@greencloud/쌈뽕하게-Lighthouse-자동화하기-ijcqk0uv#-lighthouseyml
lighthouse를 자동화 할 수 있는지조차 생각하고 있지 않았습니다. 그러던 어느날 벨로그 구천을 떠돌다가 이 글을 발견했고, (귀한 글을 적어주셔서 정말 감사합니다🫰 )
내가 굉장히 귀찮은 것을 하고 있었구나! 라고 생각이 들더군요 그래서 나도 한 번 해보자! 라는 생각으로
이 글을 작성하게 되었습니다. 그러면 그전에 lighthouse는 무엇이고, 무엇을 측정하는지 알아봅시다!
개선방안은 https://velog.io/@carloskim/Vite-최적화를-통한-사용성-개선 ← 여기 있으니 이 글도 참고해주시면 감사하겠습니다
물론 저 방법들이 전부는 아니지만 제가 어떻게 점수를 올렸는지에 대한 방법들이 있습니다 ㅎㅎ
자 그럼 본론으로 들어가보죠!
Lighthouse는 웹페이지 품질을 개선하는 데 도움이 되는 오픈소스 자동화 도구입니다. 공개 웹페이지나 인증이 필요한 모든 웹페이지에서 실행할 수 있습니다. 성능, 접근성, 프로그레시브 웹 앱, 검색엔진 최적화 등을 감사합니다.
lighthouse 공식문서
그 말대로 lighthouse 는 웹 페이지의 품질을 검사해서 여러 방면의 개선점을 알려주는 도구라고 보면됩니다!
성능과 웹사이트를 빠르게 만드는 것인데, 성능은 상대적입니다.
하지만 이러한 변동성이 있다고 해서 Lighthouse가 제시하는 개선점을 무시해서는 안 됩니다. Google의 공식 문서는 다음과 같이 강조합니다
"성능 점수의 변동성이 있을 수 있지만, Lighthouse가 제시하는 권장 사항들은 실제 사용자의 경험을 개선하는 데 검증된 방법들입니다. 각각의 권장 사항은 웹 성능에 대한 광범위한 연구와 실제 데이터를 기반으로 합니다."
따라서 Lighthouse 점수가 테스트할 때마다 조금씩 다르게 나올 수 있더라도, 도구가 지적하는 성능 개선 포인트들은 반드시 검토하고 적용을 고려해야 합니다. 이는 결과적으로 더 나은 사용자 경험을 제공하고, 웹사이트의 전반적인 품질을 향상시키는 데 도움이 될 것입니다.
그렇다면 어떤 부분을 측정하는지 확인해봅시다.
FCP는 사용자가 페이지로 이동한 후 브라우저에서 첫 번째 DOM 콘텐츠를 렌더링하는 데 걸리는 시간을 측정합니다. 페이지의 이미지, 흰색이 아닌 <canvas>
요소, SVG는 DOM 콘텐츠로 간주됩니다. iframe 내부의 콘텐츠는 포함되지 않습니다.
FCP 점수는 HTTP 자료실의 데이터를 기반으로 실제 FCP 시간을 측정하고, 이 값을 분포 위치에 따라 백분위수로 변환합니다. 백분위 수를 0 - 100 사이의 점수로 변환합니다.
속도 색인은 페이지 로드 중 콘텐츠가 시각적으로 표시되는 속도를 측정합니다.
Lighthouse는 먼저 브라우저에서 페이지 로드 동영상을 캡처하고 프레임간의 시각적 진행 상황을 계산합니다.
그러면 Lighthouse는 Speedline Node.js 모듈을 사용해 Speed Index 점수를 생성합니다.
속도 색인 점수는 HTTP 아카이브의 데이터를 기반으로 실제 웹사이트의 속도 색인을 비교합니다.
속도 색인 점수를 개선하기 위해서는
등의 방법이 있습니다.
TBT는 측정항목의 페이지 로드 속도의 측면을 나타냅니다.
페이지가 마우스 클릭, 화면 탭 또는 키보드 누름과 같은 사용자 입력에 응답하지 못하도록 차단된 총 시간을 측정합니다. 합계는 콘텐츠가 포함된 첫 페인트와 상호작용 시간 사이에 모든 장기 작업의 차단 부분을 더하여 계산됩니다.
표시 영역에서 가장 큰 콘텐츠 요소가 화면에 렌더링 될 때의 시간을 측정합니다.
페이지의 전체 수명 주기동안 발생하는 모든 예상치 못한 레이아웃의 변경에 대한 최대 버스트의 레이아웃 변경점수를 측정합니다. 레이아웃 변경은 표시되는 요소가 렌더링된 프레임에서 다음 프레임으로 위치를 변경할 때 마다 발생합니다.
웹사이트가 모든 사용자에게 얼마나 접근 가능한지를 측정합니다.
여기서 모든 사용자란?
얼마나 접근 가능한지?
웹사이트의 전반적인 코드 품질과 웹 개발 모범 사례 준수 여부를 평가합니다.
예시
등의 지표들로 측정됩니다.
웹사이트가 검색엔진에 얼마나 잘 최적화 되어 있는지를 평가합니다.
SEO 점수 개선을 위한 주된 체크리스트 입니다.
아까도 말씀드렸지만 제가 참고했던 https://velog.io/@greencloud/쌈뽕하게-Lighthouse-자동화하기-ijcqk0uv 이 블로그에 도라에몽님은 화면을 캡쳐하고 결과를 json으로 저장하고 표에 직접 작성하는 업무를 하셨기 때문에 google sheet 를 이용해서 정리를 하셨습니다.
저는 굳이 google sheet 를 이용해 정리할 필요는 없지만, 이왕한 거 끝까지 해보자! 라는 생각으로 연동까지 했고 정리해야될 필요성을 만약 못느끼신다면 pr에 댓글 남겨지는 것까지만 구현해보면 되지 않을까 싶습니다.
먼저 lighthouse 라이브러리 세팅 먼저 가보죠!
앞으로의 프로젝트에도 이 ci를 사용할 수도 있기 때문에 전역으로 설치해줍니다.
한 프로젝트에만 적용할 거라면 굳이 -g 옵션 없이도 설치할 수 있습니다.
npm install -g @lhci/cli
프로젝트의 루트에 lighthouserc.cjs
생성
Q. 오잉 ? 근데 왜 .js 파일로 안만들고 cjs 로 생성하나요?
A. 테스트를 하던 도중 에러를 마주하고 말았습니다..
require() of ES Module /Users/carloskim/cancervet_react/lighthouserc.js from /Users/carloskim/.nvm/versions/node/v20.9.0/lib/node_modules/@lhci/cli/node_modules/@lhci/utils/src/lighthouserc.js not supported.
/Users/carloskim/cancervet_react/lighthouserc.js is treated as an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which declares all .js files in that package scope as ES modules.
Instead either rename /Users/carloskim/cancervet_react/lighthouserc.js to end in .cjs, change the requiring code to use dynamic import() which is available in all CommonJS modules, or change "type": "module" to "type": "commonjs" in /Users/carloskim/cancervet_react/package.json to treat all .js files as CommonJS (using .mjs for all ES modules instead).
친절한 클로드 선생님은 lighthouserc.js가 CommonJS 형식으로 작성되어 있어서 발생하는 문제라고 하더군요
그래서 .cjs로 형식을 바꿔주었습니다.
lighthouserc.cjs
// lighthouse.config.cjs에서 페이지 이름 목록과 URL 생성 함수를 가져옴
const {
LHCI_MONITORING_PAGE_NAMES, // 모니터링할 페이지 이름들의 배열
getLhciUrlFromPageName, // 페이지 이름을 URL로 변환하는 함수
} = require("./lighthouse.config.cjs");
// 각 페이지 이름을 완전한 URL로 변환 (예: '/page/typeA' -> 'http://localhost:3000/page/typeA')
const urls = LHCI_MONITORING_PAGE_NAMES.map(
(name) => `http://localhost:3000${getLhciUrlFromPageName(name)}`
);
module.exports = {
ci: {
collect: {
// 테스트를 위한 개발 서버 시작 명령어
startServerCommand: "npm run dev",
// 검사할 URL 목록 -> 사실 위에 urls 쓰면 되는데 뭔가 에러 뱉어서 일시적으로 넣어놨습니다.
url: [
"http://localhost:3000",
"http://localhost:3000/guide",
"http://localhost:3000/introduce",
],
numberOfRuns: 1, // lighthouse 검사 실행 횟수
// lighthouse 검사 설정
// 저희 페이지는 모바일을 대응하지 않기 때문에 데스크톱만 설정해놨습니다.
settings: {
preset: "desktop", // 데스크톱 프리셋 사용
formFactor: "desktop", // 데스크톱 형식으로 검사
chromeFlags: ["--disable-mobile-emulation"], // 모바일 에뮬레이션 비활성화
screenEmulation: {
mobile: false,
width: 1350, // 화면 너비
height: 940, // 화면 높이
deviceScaleFactor: 1, // 디바이스 스케일 팩터
disabled: false, // 화면 에뮬레이션 활성화
},
},
},
assert: {
// 각 카테고리별 성능 점수 기준 설정
assertions: {
"categories:performance": ["warn", { minScore: 0.8 }], // 성능 점수 80점 이상
"categories:accessibility": ["warn", { minScore: 0.9 }], // 접근성 점수 90점 이상
"categories:best-practices": ["warn", { minScore: 0.9 }], // 모범 사례 점수 90점 이상
"categories:seo": ["warn", { minScore: 0.9 }], // SEO 점수 90점 이상
},
},
upload: {
target: "filesystem", // 결과를 파일 시스템에 저장
outputDir: "./lighthouse-results", // 결과 저장 디렉토리
// 결과 파일 이름 패턴: 페이지명-날짜시간-report.확장자
reportFilenamePattern: "%%PATHNAME%%-%%DATETIME%%-report.%%EXTENSION%%",
},
},
};
url을 만들어주는 함수가 있는데, 이것저것 에러를 해결해보다가 이렇게 사용해봤습니다.
module.exports = {
// Google Spreadsheet에 접근할 때 사용되는 Google Spreadsheet id
// Google Spreadsheet 링크가 https://docs.google.com/spreadsheets/d/12345/edit?pli=1#gid=499495518 형태라면, 그 중 12345가 Google Spreadsheet id
LHCI_GOOGLE_SPREAD_SHEET_ID: process.env.VITE_LHCI_GOOGLE_SPREAD_SHEET_ID,
// Lighthouse 점수 색상 기준
// https://developer.chrome.com/docs/lighthouse/performance/performance-scoring?hl=ko#color-coding 참고
// Lighthouse의 점수 기준을 따름
// 0 ~ 49 (빨간색): 나쁨
// 50 ~ 89 (주황색): 개선 필요
// 90 ~ 100 (녹색): 좋음
LHCI_GREEN_MIN_SCORE: 90,
LHCI_ORANGE_MIN_SCORE: 50,
LHCI_RED_MIN_SCORE: 0,
// lighthouse 성능 측정할 페이지 이름 목록
// PR Comment에 페이지 url이 아닌 페이지 이름을 노출시키기 위해 필요함
// 페이지 url이 짧다면 괜찮지만, 길다면 가독성이 떨어질 수 있기 때문에 페이지 이름을 보여주는 것을 추천
LHCI_MONITORING_PAGE_NAMES: ["guide", "home", "introduce"],
// lighthouse 성능 측정할 페이지 이름 - url 매핑
LHCI_PAGE_NAME_TO_URL: {
guide: "/guide",
introduce: "/introduce",
home: "/",
},
// lighthouse 성능 측정할 페이지 이름 - 시트 id 매핑
// Google Spreadsheet 링크가 https://docs.google.com/spreadsheets/d/12345/edit#gid=123123라면, 시트 id는 123123 부분
LHCI_PAGE_NAME_TO_SHEET_ID: {
guide: 0,
inspection: 0,
introduce: 0,
timeline: 0,
home: 0,
},
// 페이지 이름을 받아서 페이지 url을 리턴해주는 함수
getLhciPageNameFromUrl: (url) => {
for (const [name, path] of Object.entries(
module.exports.LHCI_PAGE_NAME_TO_URL
)) {
if (decodeURIComponent(path) === decodeURIComponent(url)) {
console.warn(name, "url");
return name;
}
}
},
// 페이지 url을 받아서 페이지 이름을 리턴해주는 함수
getLhciUrlFromPageName: (name) => {
return module.exports.LHCI_PAGE_NAME_TO_URL[name];
},
// 페이지 이름을 받아서 페이지 시트 id를 리턴해주는 함수
getLhciSheetIdFromPageName: (name) => {
return module.exports.LHCI_PAGE_NAME_TO_SHEET_ID[name];
},
};
참고한 블로그에서 그대로 가져왔습니다. 아주 효율적으로 잘 만들어주셔서 잘 사용하고 있습니다..!
한 파일에 전부 놓을 수도 있지만 이런식으로 모듈화를 해줄 수도 있다는 점!
참고하시면 좋을 것 같습니다.
저는 ci 를 github action 을 통해 했고 사용법은 아주 간단합니다.
루트 폴더 부터 .github > workflows 폴더 안에 lighthouse.yml을 만들어줍니다.
name: Run Lighthouse CI
on:
pull_request:
branches:
- main
types:
- opened
- synchronize
permissions:
contents: read
pull-requests: write
jobs:
lhci:
name: Lighthouse CI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Use Node.js 18.12.1
uses: actions/setup-node@v4
with:
node-version: 20.9.0
- name: Cache node_modules
uses: actions/cache@v4
id: npm-cache
with:
path: |
**/node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install packages
run: npm install
- name: Build
run: npm run build
- name: Run Lighthouse CI for Desktop
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
run: |
npm install -g @lhci/cli
lhci collect --config=lighthouserc.cjs || echo 'Fail to Run Lighthouse CI 💦'
lhci upload --config=lighthouserc.cjs || echo 'Fail to Run Lighthouse CI 💦'
- name: Format lighthouse score
id: format_lighthouse_score
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const path = require('path');
const { getLhciPageNameFromUrl, LHCI_GREEN_MIN_SCORE, LHCI_ORANGE_MIN_SCORE, LHCI_RED_MIN_SCORE } = require('./lighthouse.config.cjs');
const getColor = (score) => {
if (score >= LHCI_GREEN_MIN_SCORE) return '🟢';
else if (score >= LHCI_ORANGE_MIN_SCORE) return '🟠';
return '🔴';
};
const getAuditColorAndScore = (score) => getColor(score) + score;
const getPerformanceMetricColorAndScore = (category) => getColor(category.score * 100) + category.displayValue;
const formatResult = (res) => Math.round(res * 100);
// Lighthouse 결과 파일 경로 설정
const lighthouseDir = 'lighthouse-results';
// 결과 디렉토리 존재 확인
if (!fs.existsSync(lighthouseDir)) {
throw new Error(`Lighthouse results directory not found at ${lighthouseDir}`);
}
// 디렉토리에서 모든 json 파일 찾기
const resultFiles = fs.readdirSync(lighthouseDir)
.filter(file => file.endsWith('.json'))
.map(file => path.join(lighthouseDir, file));
if (resultFiles.length === 0) {
throw new Error('No Lighthouse result files found');
}
// 결과 파일 읽기
const desktopResults = resultFiles.map(file => {
console.log('Processing file:', file);
const content = JSON.parse(fs.readFileSync(file, 'utf8'));
console.log('Content structure:', Object.keys(content));
// Lighthouse v9+ 형식에 맞춰 수정
return {
url: content.finalUrl || content.requestedUrl || content.mainDocumentUrl || '',
jsonPath: file,
report: content
};
});
const monitoringTime = new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' });
const scoreDescription = `> 🟢: ${LHCI_GREEN_MIN_SCORE} - 100` + ' / ' + `🟠: ${LHCI_ORANGE_MIN_SCORE} - ${LHCI_GREEN_MIN_SCORE - 1}` + ' / ' + `🔴: ${LHCI_RED_MIN_SCORE} - ${LHCI_ORANGE_MIN_SCORE - 1}`;
let comments = '';
comments += `### Lighthouse report ✨\n`;
comments += `${scoreDescription}\n\n`;
const scores = { desktop: {} };
const extractLhciResults = (results, device) => {
comments += `#### ${device}\n\n`;
results.forEach((result) => {
// 디버깅을 위한 로그 추가
console.log('Result:', JSON.stringify(result, null, 2));
const { url, report } = result;
const { categories, audits } = report;
// url이 undefined인 경우 처리
if (!url) {
console.log('URL is undefined for result:', result);
return; // 이 결과는 건너뜀
}
const pageUrl = url.replace('http://localhost:3000', '');
const pageName = getLhciPageNameFromUrl(pageUrl);
const summary = {
performance: categories.performance.score,
accessibility: categories.accessibility.score,
'best-practices': categories['best-practices'].score,
seo: categories.seo.score,
pwa: categories.pwa ? categories.pwa.score : 0
};
Object.keys(summary).forEach((key) => (summary[key] = formatResult(summary[key])));
const { performance, accessibility, 'best-practices': bestPractices, seo, pwa } = summary;
const { 'first-contentful-paint': firstContentfulPaint, 'largest-contentful-paint': largestContentfulPaint, 'speed-index': speedIndex, 'total-blocking-time': totalBlockingTime, 'cumulative-layout-shift': cumulativeLayoutShift } = audits;
const formattedScoreTable = [
`| Category | Score |`,
`| --- | --- |`,
`| ${getColor(performance)} Performance | ${performance} |`,
`| ${getColor(accessibility)} Accessibility | ${accessibility} |`,
`| ${getColor(bestPractices)} Best practices | ${bestPractices} |`,
`| ${getColor(seo)} SEO | ${seo} |`,
`| ${getColor(pwa)} PWA | ${pwa} |`,
`| ${getColor(firstContentfulPaint.score * 100)} First Contentful Paint | ${firstContentfulPaint.displayValue} |`,
`| ${getColor(largestContentfulPaint.score * 100)} Largest Contentful Paint | ${largestContentfulPaint.displayValue} |`,
`| ${getColor(speedIndex.score * 100)} Speed Index | ${speedIndex.displayValue} |`,
`| ${getColor(totalBlockingTime.score * 100)} Total Blocking Time | ${totalBlockingTime.displayValue} |`,
`| ${getColor(cumulativeLayoutShift.score * 100)} Cumulative Layout Shift | ${cumulativeLayoutShift.displayValue} |`,
`\n`
].join('\n');
const score = {
Performance: getAuditColorAndScore(performance),
Accessibility: getAuditColorAndScore(accessibility),
'Best Practices': getAuditColorAndScore(bestPractices),
SEO: getAuditColorAndScore(seo),
PWA: getAuditColorAndScore(pwa),
FCP: getPerformanceMetricColorAndScore(firstContentfulPaint),
LCP: getPerformanceMetricColorAndScore(largestContentfulPaint),
'Speed Index': getPerformanceMetricColorAndScore(speedIndex),
TBT: getPerformanceMetricColorAndScore(totalBlockingTime),
CLS: getPerformanceMetricColorAndScore(cumulativeLayoutShift)
};
scores[device][pageName] = score;
comments += `<details>\n<summary>${pageName}</summary>\n\n> ${pageUrl}\n\n${formattedScoreTable}\n</details>\n\n`;
});
};
extractLhciResults(desktopResults, 'desktop');
core.setOutput('comments', comments);
core.setOutput('monitoringTime', monitoringTime);
core.setOutput('scores', scores);
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
// github 객체는 actions/github-script에서 자동으로 제공됨
const { repo, payload } = context;
const { data: previousComments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number
});
const previousLhciComment = previousComments.find((comment) =>
comment.body.startsWith(`### Lighthouse report ✨\n`)
);
const newComment = `${{ steps.format_lighthouse_score.outputs.comments }}`;
if (previousLhciComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: previousLhciComment.id,
body: newComment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body: newComment
});
}
# google sheet에 기록할 필요가 없으시면 위에까지만 사용하시면 됩니다.
- name: Install required packages
run: npm install google-spreadsheet google-auth-library
- name: Update Google SpreadSheet
uses: actions/github-script@v7
with:
script: |
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { JWT } = require('google-auth-library');
async function updateGoogleSheet() {
try {
console.log('Using service account email:', '${{ secrets.LHCI_GOOGLE_CLIENT_EMAIL }}');
const serviceAccountAuth = new JWT({
email: '${{ secrets.LHCI_GOOGLE_CLIENT_EMAIL }}',
key: '${{ secrets.LHCI_GOOGLE_PRIVATE_KEY }}'.replace(/\\n/g, '\n'),
scopes: [
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive.file'
]
});
const doc = new GoogleSpreadsheet('${{ secrets.LHCI_GOOGLE_SPREAD_SHEET_ID }}', serviceAccountAuth);
await doc.loadInfo();
console.log('Successfully loaded spreadsheet:', doc.title);
const { desktop } = ${{ steps.format_lighthouse_score.outputs.scores }};
const monitoringTime = '${{ steps.format_lighthouse_score.outputs.monitoringTime }}';
const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/pull/${context.payload.pull_request.number}`;
const prHyperlink = `=HYPERLINK("${prUrl}", "#${context.payload.pull_request.number}")`;
// 각 페이지별 시트 업데이트
for (const pageName in desktop) {
console.log(`Processing page: ${pageName}`);
// 시트 찾기
const sheet = doc.sheetsByTitle[pageName];
if (!sheet) {
console.warn(`Warning: Sheet '${pageName}' not found in spreadsheet`);
continue;
}
console.log(`Found sheet for ${pageName}:`, sheet.title);
const scores = desktop[pageName];
// 새 행 데이터 준비
const newRow = {
'PR url': prHyperlink,
'Monitoring Time': monitoringTime,
'Performance [D]': scores.Performance,
'Accessibility [D]': scores.Accessibility,
'Best Practices [D]': scores['Best Practices'],
'SEO [D]': scores.SEO,
'PWA [D]': scores.PWA,
'FCP [D]': scores.FCP,
'LCP [D]': scores.LCP,
'Speed Index [D]': scores['Speed Index'],
'TBT [D]': scores.TBT,
'CLS [D]': scores.CLS
};
try {
// 기존 행 찾기
const rows = await sheet.getRows();
const existingRow = rows.find(row =>
row['PR url'] && row['PR url'].includes(`#${context.payload.pull_request.number}`)
);
if (existingRow) {
console.log(`Updating existing row for PR #${context.payload.pull_request.number} in ${pageName}`);
Object.keys(newRow).forEach(key => {
existingRow[key] = newRow[key];
});
await existingRow.save();
} else {
console.log(`Adding new row for PR #${context.payload.pull_request.number} in ${pageName}`);
await sheet.addRow(newRow);
}
console.log(`Successfully updated sheet for ${pageName}`);
} catch (err) {
console.error(`Error processing sheet ${pageName}:`, err.message);
}
}
} catch (error) {
console.error('Error details:', error);
core.setFailed(`Failed to update Google Sheet: ${error.message}`);
}
}
await updateGoogleSheet();
스크립트의 대략적인 설명은 이러합니다.
전에 도라에몽님의 스크립트와 살짝 다른점은 경로 부분이 다르고, 사용되는 메서드가 조금 다릅니다.
도라에몽님께서 써주신 스크립트를 보면 아래처럼 되어있습니다.
근데 인증하는 부분에서 doc.useServiceAccountAuth(creds); is not function
이라는 오류가 발생했습니다. 그래서 검색해보니까 인증 방식이 조금 달라졌더라구요?
//before
const fs = require('fs');
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { LHCI_GOOGLE_SPREAD_SHEET_ID, getLhciSheetIdFromPageName } = require('./src/configs/lighthouse/Lighthouse.js');
const updateGoogleSheet = async () => {
const creds = {
client_email: `${{ secrets.LHCI_GOOGLE_CLIENT_EMAIL }}`,
private_key: `${{ secrets.LHCI_GOOGLE_PRIVATE_KEY }}`,
};
const { desktop, mobile } = ${{ steps.format_lighthouse_score.outputs.scores }};
const monitoringTime = `${{ steps.format_lighthouse_score.outputs.monitoringTime }}`;
const { repo, payload } = context;
const doc = new GoogleSpreadsheet(LHCI_GOOGLE_SPREAD_SHEET_ID);
await doc.useServiceAccountAuth(creds);
//present
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { JWT } = require('google-auth-library');
async function updateGoogleSheet() {
try {
console.log('Using service account email:', '${{ secrets.LHCI_GOOGLE_CLIENT_EMAIL }}');
const serviceAccountAuth = new JWT({
email: '${{ secrets.LHCI_GOOGLE_CLIENT_EMAIL }}',
key: '${{ secrets.LHCI_GOOGLE_PRIVATE_KEY }}'.replace(/\\n/g, '\n'),
scopes: [
'https://www.googleapis.com/auth/spreadsheets',
'https://www.googleapis.com/auth/drive.file'
]
});
const doc = new GoogleSpreadsheet('${{ secrets.LHCI_GOOGLE_SPREAD_SHEET_ID }}', serviceAccountAuth);
await doc.loadInfo();
console.log('Successfully loaded spreadsheet:', doc.title);
또한 현재 스크립트는 desktop만 고려하고 있지만, 앞으로의 프로젝트에서는 mobile도 추가해서
확장성있게 사용할 수 있다는 점이 너무 좋았습니다.
여기서 사용되는 환경변수는 4가지가 있습니다.
어떻게 세팅해야하는지 한 번 가보시죠!
https://github.com/apps/lighthouse-ci
에 접속합니다
configure 누르시고 해당하는 프로젝트를 선택하시고 토큰 값을 받으신 다음에 해당 레포지토리로 들어가서
settings > secrets and variables 에서 new repository secret 을 누르시고
LHCI_GITHUB_APP_TOKEN 값에 아까 복사한 값을 넣어주시면 됩니다!
PR만 하신다면 이 단계는 필요없습니다!
https://velog.io/@greencloud/쌈뽕하게-Lighthouse-자동화하기-ijcqk0uv#-google-spreadsheet-api-설정 ← 이 해쉬링크를 타고 들어가시면 이미지로 아주 잘 설명되어있습니다.
이 단계를 따라가다 보면 json 파일을 받을 수 있는데 그 json 파일을 잘 받아서 환경변수에 넣고 secret으로 넣어주시면 됩니다.
위의 과정을 모두 마치셨으면 스프레드 시트는 이미 생성된 상태일 것입니다.
spread_sheet_id는 만약에 주소가
https://docs.google.com/spreadsheets/a/carlos/edit?gid=0#gid=0
라면? carlos 라는 값을 환경변수 값에 넣어주시면 됩니다.
저는 guide , home , introduce 라는 페이지에 대한 라이트하우스를 체크해야겠다고 생각했습니다.
그래서 세 페이지에 대한 구글 시트를 생성했고, 여기서 놓치면 안되는 점은
체크해야하는 페이지에 따라서 시트를 미리 만들어줘야하고, 이름도 같아야 저기에 알맞게 생성됩니다!
그렇기 때문에 이 작업은 꼭 빼먹지 말고 시트 미리 만들어주시면 됩니다!
페이지 별로 구글 시트에 정리가 된 모습
빨간불이 너무 많이 들어와서 부끄럽지만 셀프 회초리하는 생각으로 올립니다..
github action 이 pr 에 댓글을 달아준 모습입니다.
아주 체크가 잘 되어 있습니다.
그대로 도라에몽님의 글을 보고, 카카오 엔터테이먼트의 블로그도 참고했는데도 삽질을 꽤나 많이 했습니다.
lighthouse로 일일이 테스트 할 필요없이 자동화 할 수 있음에 너무 기쁘고 참고한 블로그의 주인 분들에게도
좋은 글 써주셔서 너무 감사드린다는 말씀 전해드리고 싶습니다.
제 스스로 한 가지 아쉬운 점은 토큰을 사용해야하는 페이지에서는 저 스크립트를 사용할 수는 없었습니다..
저희 회사가 b2b 인지라 로그인을 통해서 사용하는 페이지가 메인이자 가장 최적화가 필요한 곳인데 말이죠..😢
근데 그렇다고해서 라이트 하우스 매 순간 체크할꺼냐? 그것은 결례입니다
클로드 선생님에게 여쭤보니 MSW로 모킹을 해서 테스트를 해봐라 하시더라구요?
서버 모킹을 통해서 테스트를 해라… 새로운 과제가 생겨서 너무 기쁩니다… 하핫
다음에는 MSW를 사용해서 토큰있는 곳은 어떻게 라이트 하우스를 체크하는지 공부해서 돌아오겠습니다!!
[출처]
https://velog.io/@greencloud/쌈뽕하게-Lighthouse-자동화하기-ijcqk0uv#-lighthouseyml
https://fe-developers.kakaoent.com/2022/220602-lighthouse-with-github-actions/
https://developer.chrome.com/docs/lighthouse/performance/first-contentful-paint?hl=ko
https://web.dev/articles/user-centric-performance-metrics?hl=ko