Devin.KR

렌더링 비용 줄이기

130분 안팎

학습 목표

큰 이미지와 DOM 갱신의 비용을 비교합니다.

개념

바뀐 카드의 경계를 찾습니다

검색 함수가 빨라도 많은 카드 객체를 다시 만들면 화면 작업이 늘어날 수 있습니다. 그러나 카드가 1000개라는 사실만으로 병목이라고 단정하지 않습니다. 앞 레슨의 고정 구간 기록에서 계산·DOM 갱신·후속 레이아웃을 구분하고 어떤 비용을 줄일지 정합니다. 이번 변경의 가설은 동일 목록을 다시 그릴 때 내용이 같은 카드 객체를 재사용하면 생성 작업을 줄일 수 있다는 것입니다.

기존 renderList는 배열의 모든 행사를 EventCard로 만든 뒤 replaceChildren으로 넣습니다. 제목 한 개가 바뀌어도 다른 카드까지 새 객체가 됩니다. 변경 후에는 행사 id로 이전 카드 후보를 찾고 제목·지역·날짜·링크 URL을 비교합니다. 출력에 영향을 주는 값이 같으며 지금 목록 안에 붙어 있는 카드라면 재사용합니다. 그 외에는 새로운 카드를 만듭니다.

id만 같으면 그대로 두는 구현은 불충분합니다. 같은 행사의 이름이 바뀌거나 날짜가 바뀌었는데 옛 내용이 보일 수 있습니다. 링크 URL도 검색어와 지역 조건을 담으므로 변경 감지에 포함합니다. 상세로 이동한 뒤 돌아왔을 때 조건을 잃는 결함은 단순한 문구 오류보다 탐색 흐름을 더 크게 해칩니다. signature는 실제 출력에 쓰는 필드들의 배열을 JSON.stringify한 값입니다.

문자열을 구분자 하나로 연결하면 제목에 그 구분자가 있을 때 서로 다른 값이 같아질 수 있습니다. 배열을 JSON으로 직렬화하면 필드 경계를 유지할 수 있습니다. 캐시의 키는 행사 id이고 signature는 갱신 여부를 확인하는 내용입니다. 객체 참조만 비교하지 않는 이유는 응답마다 같은 내용의 새 객체가 올 수 있기 때문입니다. 값이 같은 복사본으로도 재사용 검사를 합니다.

WeakMap은 목록 요소별 캐시를 보관하는 용도로 사용합니다. 이 캐시가 목록 키만으로 그 요소의 수명을 연장하지 않도록 설계합니다. 별도 전역 배열에 카드를 계속 쌓으면 WeakMap을 쓰더라도 다른 강한 참조가 남습니다. 다음 캐시에는 현재 결과에 있는 id만 넣어 사라진 항목을 누적 보관하지 않습니다. 캐시 전략의 메모리 비용도 변경 설명에 포함합니다.

캐시 후보가 현재 목록에 붙어 있는지도 확인합니다. 앱은 loading·error 전환에서 목록을 비워 오래된 결과를 노출하지 않습니다. 이때 카드가 제거되면 복귀 시 새 카드를 만드는 기존 계약을 유지합니다. 캐시만 보고 제거된 노드를 되살리면 이전 포커스 회귀 검사의 의미가 바뀔 수 있습니다. 이번 최적화는 요청 사이 보존이 아니라 이미 붙은 동일 목록의 재갱신에 한정합니다.

매 카드마다 전체 children을 배열로 만들고 includes로 찾으면 큰 목록에서 반복 탐색 비용이 커집니다. 호출 시작에서 현재 자식을 Set으로 한 번 만들고 has로 포함 여부를 판단합니다. 생성 작업을 줄이려다가 다른 큰 반복을 더하지 않는지 확인합니다. 전체 소요 시간은 signature 계산·Map 관리·목록 재배치까지 포함하므로 작은 목록에서는 개선이 없을 수도 있습니다.

순서가 바뀌면 같은 카드 객체를 새 순서로 넣습니다. 같은 id의 카드를 유지하는 것과 순서를 유지하는 것은 다릅니다. 이번 API의 결과 배열 순서가 화면 순서의 기준입니다. 반례는 [1,2]를 [2,1]로 바꾸면서 카드 객체 참조는 유지되는지 확인합니다. 제목 변경·지역 변경·날짜 변경·href 변경·빈 목록도 따로 넣어 빠른 경로가 오래된 표시를 남기지 않는지 검사합니다.

replaceChildren은 이 과제에서도 호출합니다. 따라서 모든 DOM 변경과 레이아웃 비용을 없앴다고 말할 수 없습니다. 줄이는 대상은 내용이 같은 EventCard 생성입니다. 가상 스크롤이나 페이지 나누기는 표시되는 결과 수와 키보드 탐색 계약까지 바꾸므로 별도 설계가 필요합니다. 이번 과제에서 카드 일부를 임의로 잘라 속도를 높이는 방식은 완료 기준을 만족하지 않습니다.

이미지는 픽셀 크기뿐 아니라 다운로드 크기와 표시 공간을 나눠 봅니다. 제공 event.svg의 원본은 1200×600 비율입니다. width와 height를 선언하고 CSS의 max-width:100%와 height:auto로 축소해 비율을 유지합니다. 공간 예약은 다운로드 바이트를 줄이는 것과 다른 개선입니다. 큰 사진을 다룰 때는 용도에 맞는 해상도·포맷·반응형 소스를 검토하고 전송량도 측정합니다.

이번 카드는 제목과 메타가 이미 행사 정보를 전달하며 이미지도 같은 교육용 장식입니다. 중복 안내를 줄이려고 alt는 빈 문자열로 둡니다. 실제 포스터에 날짜나 장소처럼 다른 정보가 담기면 대체 텍스트 정책을 다시 정해야 합니다. 모든 이미지의 alt를 비워도 된다는 규칙으로 확장하지 않습니다. 카드 생성은 textContent를 사용해 외부 제목을 HTML 코드로 해석하지 않습니다.

이미지를 첫 화면에 바로 보여야 하는지 판단하지 않고 모든 이미지에 lazy를 붙이지 않습니다. 첫 화면의 중요한 이미지까지 늦추면 초기 표시가 더 느려질 수 있습니다. 이번 해답은 크기·비율 선언에 집중하며 로딩 정책은 별도 측정 후보로 둡니다. 브라우저의 느린 네트워크에서 이미지 도착 전후 카드가 밀리는지 관찰하고 Node의 width 속성 검사와 실제 배치 관찰을 구분합니다.

render-test는 카드 객체 참조 유지와 변경된 내용 표시를 같이 확인합니다. 생성 수만 줄었는데 링크가 옛 조건이라면 개선으로 받지 않습니다. AssertionError에서 서로 달라야 할 카드가 같은 참조라면 signature 누락을 찾습니다. 같은 객체여야 하는데 달라졌다면 id 키·현재 자식 검사·복사본 비교를 봅니다. TypeError가 나면 테스트 모델에 없는 DOM API를 무심코 썼는지도 확인합니다.

performance-browser.html은 두 버튼으로 전체 재생성과 카드 재사용을 비교합니다. 1000개 고정 카드의 준비 렌더 뒤 동기 갱신 세 번을 기록하며 계산된 중앙값을 보여 줍니다. 이 짧은 구간은 페인트를 포함하지 않습니다. 개발자 도구의 타임라인으로 뒤따르는 레이아웃과 페인트도 관찰하고 실제 results.html의 검색 동작에 같은 효과가 있는지 별도로 확인합니다.

브라우저 비교 전후에는 같은 이미지 생성 정책을 사용합니다. 크기를 바꾸고 캐시도 바꾸면 무엇이 효과를 냈는지 흐려집니다. 하나씩 바꾼 실험과 최종 조합의 실험을 구분합니다. 미개선은 실패를 숨길 이유가 아니라 적용 범위를 좁힐 근거입니다. 전체 검색 흐름이 목록을 매번 비우므로 반복 renderList의 이익이 실제 앱에서는 제한될 수 있음을 리뷰에 남깁니다.

완료는 dom.mjs와 components.mjs의 TODO를 구현하고 기존 검색·빈 상태·상세 복귀·요청 순서 검사를 유지하는 것입니다. 코드 줄 수나 모델 시간 한 번으로 판단하지 않습니다. 원자료·바뀐 카드 반례·이미지 비율·실제 관찰 상태를 함께 제출합니다. 서재의 성능 장에서는 더 넓은 렌더링 전략을 읽고 여기서는 선택한 작은 변경의 범위와 비용을 설명합니다.

API와 명령 세부 확인: img 크기와 대체 텍스트를 참고합니다.

따라하기

URL 변경을 캐시 키에 반영합니다

독립 예제를 demo.cjs에 저장하고 node demo.cjs로 실행합니다. 출력의 의미를 본문 계약과 비교합니다.

const before={id:1,name:'책',href:'/event?q=책'};const after={...before,href:'/event?q=전시'};const signature=x=>JSON.stringify([x.name,x.href]);console.log(signature(before)===signature(after));console.log(before.id===after.id);

실행 결과

false
true

현재 자식과 이미지 비율을 확인합니다

독립 예제를 demo.cjs에 저장하고 node demo.cjs로 실행합니다. 출력의 의미를 본문 계약과 비교합니다.

const current=new Set([1,2]);console.log(current.has(2));console.log(current.has(3));console.log(1200/600);

실행 결과

true
false
2

카드 재사용과 이미지 공간을 구현합니다

dom.mjs에 현재 자식 Set과 id별 signature 캐시를 추가하고 components.mjs에 이미지 크기·비율을 선언합니다. 카드 내용과 URL 변경 반례를 유지합니다.

npm test

실제 브라우저 비교를 관찰합니다

외부 환경에서 npm start 후 performance-browser.html을 열고 같은 조건으로 전후 버튼을 비교합니다. 실제 results.html 검색과 이미지 도착 전후 배치도 확인합니다.

확인 문제

실습

dom.mjs의 renderList와 components.mjs를 보완합니다. 동일 카드 재사용·제목/지역/날짜/URL 변경·순서·빈 목록·외부 제거·이미지 비율을 확인합니다. 실제 배치·레이아웃·페인트는 브라우저에서 따로 검증합니다.

시작 코드·테스트 내려받기

실행 명령

npm test

기대 결과

starter는 해당 주제 assertion 실패, solution은 기존 회귀와 새 모델 검사 전부 통과

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 컴포넌트의 상태 위치를 정한 사례를 설명합니다.