- Published on
[번역] 프론트엔드 성능 패턴
- Authors
- Name
- Easyoon
Frontend performance pattern
왜 이 글을 읽어야 할까요?
- 프론트엔드 성능 최적화에 사용되는 일반적인 패턴
- 웹앱 속도 향상
- 상사 및 동료 설득
성능을 언제 신경써야 할까요?
저에게 프론트엔드 앱은 세 가지 측면을 유지 관리해야 합니다.
- 기능적 - 앱은 올바른 논리로 실행되어야 합니다.
- 유지 보수 가능/읽기 쉬움 - 앱이 올바르게 실행되면 유지 관리 및 새로운 기능 추가가 쉬워야 합니다.
- 성능 - 빠르고 사용자 여정을 즐겁게 해야 합니다.
언제 성능에 관심을 가져야 할까요?
우선, 우리가 앱의 시스템, 구조를 설계할 때마다 우리는 트레이드오프를 한다는 것을 의미한다는 데 동의합시다. 우리는 시스템을 문제에 맞게 만들기 위해 어떤 부분을 잘라내고 다른 부분을 얻습니다. 만약 우리가 모든 것을 원한다면 어떨까요? 프로젝트를 시작할 때 리소스는 항상 제한되어 있기 때문에 불가능합니다. 그러나 그 반대의 문제는 끝없이 커질 수 있습니다.
- Centralize vs Decentralize
- Monolithic vs Microservice
- SSR vs CSR
- OOP vs FP
- SQL vs No-SQL
- Language X vs Language Y
- API vs GraphQL
- Stream vs Batch
그러면 언제 성능에 대해 신경 써야 할까요? 그리고 어떤 것이 득과 실의 관계일까요? 나에게 프론트엔드 앱은 3가지 측면을 유지해야 합니다.
- 기능적 - 앱은 올바른 논리로 실행되어야 합니다.
- 유지 관리 가능/읽기 용이 - 올바르게 실행되면 유지 관리가 쉽고 새로운 기능을 추가할 수 있어야 합니다.
- 성능 - 빠르고 사용자 여정을 즐겁게 해야 합니다.
기능성은 이해하기 쉽습니다. 이것은 우리가 트레이드오프할 수 없는 유일한 측면입니다. 따라서 이제 우리의 애플리케이션은 깔끔한 코드와 성능 사이의 슬라이더가 됩니다. 이는 각 프로젝트와 문제에 따라 트레이드오프가 달라집니다.
🐣 프로젝트에 참여하는 개발자로서, 무엇이 더 중요한지 알 권리가 있습니다. 빠른 속도로 많은 기능을 출시하기를 원하십니까? 아니면 기능이 매우 빠르게 실행되기를 원하십니까?
"성능에 너무 집착했던 적이 있고, 많은 주니어 개발자들도 저와 같은 집착을 갖고 있다는 것을 알게 되었습니다. 예를 들어, 한때 CSV 파일을 검증하는 스크립트를 병렬 처리하는 데 하루를 보낸 적이 있습니다. 최적화 결과는 좋아 보였고, 실행 시간이 15분에서 4분으로 줄어들었습니다. 하지만 안타깝게도 이 스크립트는 일주일에 한 번만 실행되며, 결과적으로 무료 서버에서 한 달에 약 40분 정도만 절약할 수 있었습니다."
Split code/Lazyload
난이도: 쉬움 적용 시기: 프로젝트 시작 즉시 페이지/루트별로 코드를 분할하는 것과 같은 간단한 방법부터 시작할 수 있습니다. 이후 더 나아가고 싶다면 사용자 상호 작용에 따라 코드를 분할할 수 있습니다. 주문: 사용자에게 필요한 것만 로드합니다. 방법: 프레임워크에 따라 다르므로, Google에서 다음 공식으로 검색하십시오: Framework + 코드 분할
React 예시 React 공식 문서를 참고하세요.
import React, { Suspense } from 'react';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
중복 된 라이브러리 설치 방지
난이도: 매우 쉬움
적용 시기: 새로운 라이브러리를 설치하려고 고민할 때. 다음 세 가지 옵션이 있습니다.
- 기존 라이브러리를 사용하는 경우, 문제에 맞게 사용하기를 바랍니다.
- 새로운 라이브러리를 사용하는 경우, 레거시 코드를 변경하고 레거시 문제에 맞게 사용하기를 바랍니다. 회귀 버그 테스트를 수행합니다.
- 두 라이브러리를 모두 사용합니다. ⇒ 이것은 마지막 희망입니다... 마치 PM이 목에 칼을 대고 있는 것처럼.
현재 프로젝트에서는 날짜/시간을 처리하기 위해 moment
, date-fns
및 dayjs
라는 3개의 라이브러리를 사용하고 있습니다. moment
와 date-fns
는 번들 크기가 큽니다. You might not need date fns를 참고하세요. 주문: 새 라이브러리를 검색/설치하기 전에 packages.json을 확인하세요.
ES6 및 트리 쉐이킹을 지원하는 라이브러리를 선택하세요
난이도: 쉬움, 하지만 커뮤니티에 따라 다름 적용 시기: 항상. 번들 크기와 트리쉐이킹 지원은 고려해야 할 중요한 사항입니다. 주문: 최신 라이브러리일수록 더 나을 가능성이 높습니다. (그러나 안정적이고 올바르다는 보장은 없습니다.) 방법: https://bundlephobia.com/ 에서 라이브러리를 체크 하세요.
사용자 입력 디바운스
난이도: 매우 쉬움
적용 시기: 사용자 입력(타이핑, 스크롤 이벤트 등)을 처리할 때 몇 가지 작업을 수행해야 하는 경우
주문: 검색 입력 ⇒ 디바운스
디바운스란? (번역자가 추가한 내용)
웹용 프로그램에서 입력 처리 함수는 프레임 완료를 차단하고 불필요한 레이아웃 작업을 발생시킬 수 있으므로 성능 문제의 잠재적인 원인이 될 수 있습니다. 오래 실행되는 입력 처리 함수는 스크롤링을 차단할 수 있으므로 피해야 합니다. 또한 입력 처리 함수에서 스타일 변경을 수행하지 마십시오.
디바운스의 장점:
- 성능 향상: 사용자 입력에 대한 작업을 지연시킴으로써 브라우저가 다른 작업(예: 화면 렌더링, 이벤트 처리)을 수행할 수 있도록 해줍니다.
- 입력 감소: 빠르게 입력하는 사용자의 경우 불필요한 작업을 줄입니다.
예시:
- 검색 필터: 사용자가 검색 창에 입력할 때마다 API를 호출하는 대신, 사용자가 입력을 완료한 후 또는 일정 시간이 지난 후에만 API를 호출하도록 합니다.
- 실시간 채팅: 사용자가 메시지를 입력할 때마다 서버에 메시지를 전송하는 대신, 사용자가 입력을 완료한 후에만 메시지를 전송하도록 합니다.
참고자료: Debounce Your Input Handlers 더 깊은 이해: 컴퓨터 성능이 낮은 경우 API 응답에도 디바운스를 사용할 수 있습니다. 예를 들어, 거래 또는 주문簿에 대한 응답을 디바운스하는 것이 일반적인 경우입니다.
태그 img, iframe에 loading=lazy 추가
난이도: 쉬움
적용 시기: 대부분의 경우 img 태그를 볼 때 적용합니다. 단, img가 접힘 위에 있는 경우에는 적용하지 않아도 됩니다.
주문: 이미지 + loading=lazy ⇒ ✈️
메모이제이션 함수
난이도: 보통
적용 시기: 함수 실행에 많은 CPU와 RAM 리소스를 소모하는 경우
주문: 값비싼 작업 캐싱
메모이제이션이란? (번역자가 추가한 내용)
함수는 프로그램에서 중요한 부분입니다. 코드의 모듈성과 재사용성을 높여줍니다. 프로그램을 함수 단위로 나눠 나중에 호출하여 유용한 작업을 수행하는 것이 일반적입니다. 하지만 함수를 여러 번 호출하는 경우 비용이 많이 드는 경우가 있습니다.
메모이제이션을 사용하는 이유:
- 성능 향상: 동일한 입력값으로 함수를 여러 번 호출하는 경우 계산 결과를 캐싱하여 재사용함으로써 성능을 향상시킬 수 있습니다.
- 효율성 증가: 캐싱을 통해 함수 실행 시간을 줄이고 프로그램 전체적인 효율성을 높일 수 있습니다.
예시:
- 큰 수열의 피보나치 수 계산
- 복잡한 데이터 처리 작업
참고자료:
- Understanding Memoize in JavaScript | freeCodeCamp.org
추가적인 방법:
- 웹 워커를 사용하여 계산 작업을 백그라운드 프로세스로 분리할 수도 있습니다.
const cachedResult = useMemo(() => {
// CPU intensive task here
}, [dependencies]);
프론트엔드 성능 최적화 패턴
서비스 워커를 사용한 프론트엔드 자산 캐싱
난이도: 보통, 어려움 (처음 사용하기는 어렵지만 효과는 매우 좋음)
적용 시기: 매우 큰 규모의 웹 앱을 다루고 있으며 번들 크기가 큰 경우(복잡한 관리자/CRM 등)
주문: 복잡하고 큰 웹 앱 ⇒ 서비스 워커
예시 (리액트):
리액트 PWA 활성화 설정 with Workbox
리액트를 사용하여 PWA를 만드는 것은 쉽습니다! 하지만 실제로 기능을 추가하고 싶지 않은 경우를 제외하면 말이죠. 이 글에서는 설치 가능성, 사전 캐싱, 백그라운드 동기화, 푸시 알림 등 실제 프로그레시브 웹 앱의 모든 기능을 갖춘 리액트 및 Workbox를 사용하여 PWA를 만들 수 있도록 안내합니다.
참고자료:
장점:
- 사용자는 처음 로딩 화면만 보고 나머지 과정은 오프라인 상태에서도 이용할 수 있습니다.
- 백그라운드에서 앱을 업데이트할 수 있습니다.
가상 리스트
난이도: 어려움
적용 시기: 많은 항목을 포함하는 목록을 다루고 있고 사용자가 모든 항목을 보기 위해 스크롤해야 하는 경우
주문: 100개 이상의 항목이 있는 테이블, Facebook이나 Twitter 피드와 같은 목록 구현 시 ⇒ 가상 리스트
리액트 가상 라이브러리:
리액트 가상 라이브러리는 개발자의 편의를 위해 설계되었습니다. 가상 라이브러리는 역제어를 사용하며 개발자가 실제적인 컨트롤을 유지할 수 있습니다. 뭔가를 커스터마이징하고 싶다면 그 방법을 제공합니다. 하지만 필요한 것 이상을 요구하지 마십시오!
참고자료:
추천 사항:
가상 리스트를 사용할 때는 개발자가 가상 리스트가 작동하는 방식과 컴포넌트가 다시 렌더링되는 시점을 이해해야 최대 효과를 낼 수 있습니다. 그렇지 않으면 오히려 성능 저하를 초래할 수 있습니다.
장기 실행 기능을 여러 개의 단기 실행 기능으로 분할
난이도: 어려움
적용 시기: 함수를 실행했을 때 노트북이 멈췄을 때
주문: 위와 같음
방법: 장기 실행 함수를 setTimeout
또는 requestAnimationFrame
을 사용하여 여러 단기 실행 함수로 분할합니다. 하지만 장기 실행 함수를 많은 작은 함수로 분할하는 것은 쉬운 작업이 아니며, 때로는 함수가 순차적으로 실행되도록 유지해야 함수가 항상 올바른 결과를 반환하도록 보장해야 합니다.
낙관적 업데이트
어려움: 쉽거나 보통 또는 어려움
- 간단한 엔티티에 적용할 때는 쉽습니다.
- 로컬 상태와 서버 상태 간의 충돌이 발생할 때는 보통 난이도입니다.
- 로직이 복잡하고 로컬 상태와 서버 상태 간의 충돌을 해결해야 할 때는 어렵습니다.
- 예시: 좋아요 버튼은 쉽고, 댓글은 보통, 상태 게시는 매우 어렵습니다.
적용 시기: 기능이 매우 간단하고 API 성공률이 약 99.99%에 가까울 때
주문: 간단한 로직, 99.99% 성공 ⇒ 낙관적 업데이트
낙관적 UI 구축을 통한 번개처럼 빠른 프론트엔드
웹 앱을 더 빠르게 구축하기 위한 끝없는 노력 속에서 어떤 옵션도 제외될 수 없습니다. 우리는 읽기와 쓰기를 최적화하기 위해 데이터베이스를 분할하고, 서비스를 수요에 따라 확장 및 축소하며, 이 모든 것 위에 복잡한 캐싱 전략을 사용합니다.
참고자료:
- 낙관적 UI 구축을 통한 번개처럼 빠른 프론트엔드: https://derekndavis.com/posts/lightning-fast-front-end-build-optimistic-ui
지연된 폴리필/동적 폴리필
난이도: 보통, 어려움
적용 시기: 더 이상 최적화할 방법이 없을 때
주문: 폴리필 번들 크기가 매우 크지만 사용자가 모두 최신 기술을 사용하는 경우
방법: 현재 가장 많이 사용되는 방법은 Polyfill.io입니다. 하지만 프론트엔드와 백엔드 모두 설정해야 하므로 다소 복잡합니다.
어떤 패턴을 사용하여 성능을 최적화하시나요? 함께 더 많은 것을 알아보고 싶습니다.