숙련 과정 프런트 엔드(vue) 진입: JavaScript를 배우기 위해 진입하면 메모리 관리 작업이 제공되지 않습니다. 대신 메모리는 가비지 수집 이라는 메모리 회수 프로세스를 통해 JavaScript VM에서 관리됩니다.
가비지 수집을 강제할 수는 없는데 작동하는지 어떻게 알 수 있나요? 우리는 그것에 대해 얼마나 알고 있나요?
이 프로세스 중에는 스크립트 실행이 일시 중지됩니다.
액세스할 수 없는 리소스에 대한 메모리를 해제합니다.
불확실하다
전체 메모리를 한 번에 검사하는 것이 아니라 여러 주기로 실행됩니다.
예측할 수 없지만 필요할 때 수행됩니다.
이는 리소스 및 메모리 할당 문제에 대해 걱정할 필요가 없다는 것을 의미합니까? 주의하지 않으면 일부 메모리 누수가 발생할 수 있습니다.
메모리 누수는 소프트웨어가 회수할 수 없는 할당된 메모리 블록입니다.
Javascript는 가비지 수집기를 제공하지만 이것이 메모리 누수를 피할 수 있다는 의미는 아닙니다. 가비지 수집 대상이 되려면 개체를 다른 곳에서 참조하면 안 됩니다. 사용되지 않은 리소스에 대한 참조를 보유하면 해당 리소스가 회수되지 않습니다. 이것을 무의식적인 기억 유지 라고 합니다.
메모리 누수로 인해 가비지 수집기가 더 자주 실행될 수 있습니다. 이 프로세스로 인해 스크립트가 실행되지 않으므로 프로그램이 정지될 수 있습니다. 이러한 지연이 발생하면 까다로운 사용자는 만족하지 않으면 제품이 오랫동안 오프라인 상태가 될 것임을 분명히 알 수 있습니다. 더 심각한 것은 전체 애플리케이션이 충돌할 수 있다는 것입니다.
메모리 누수를 방지하는 방법 가장 중요한 것은 불필요한 리소스를 유지하지 않는 것입니다. 몇 가지 일반적인 시나리오를 살펴보겠습니다.
setInterval()
메서드는 각 호출 사이에 고정된 시간 지연을 두고 반복적으로 함수를 호출하거나 코드 조각을 실행합니다. 간격을 고유하게 식별 ID
간격 ID
반환하므로 나중에 clearInterval()
호출하여 삭제할 수 있습니다.
x
회 루프 후에 완료되었음을 나타내기 위해 콜백 함수를 호출하는 구성 요소를 만듭니다. 이 예에서는 React를 사용하고 있지만 이는 모든 FE 프레임워크에서 작동합니다.
import React, 'react'에서 { useRef }; const 타이머 = ({ cicles, onFinish }) => { const currentCicles = useRef(0); setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); 반품; } currentCicles.current++; }, 500); 반품 ( <p>로드 중...</p> ); } 기본 타이머 내보내기;
얼핏 보면 아무런 문제가 없어 보인다. 걱정하지 마세요. 이 타이머를 트리거하고 메모리 성능을 분석하는 또 다른 구성 요소를 만들어 보겠습니다.
import React, 'react'에서 { useState }; '../styles/Home.module.css'에서 스타일 가져오기 '../composites/Timer'에서 타이머를 가져옵니다. 기본 함수 내보내기 Home() { const [showTimer, setShowTimer] = useState(); const onFinish = () => setShowTimer(false); 반품 ( <p className={styles.container}> {쇼타이머( <타이머 cicles={10} onFinish={onFinish} /> ): ( <버튼 onClick={() => setShowTimer(true)}> 다시 해 보다 </버튼> )} </p> ) }
Retry
버튼을 몇 번 클릭한 후 Chrome 개발자 도구를 사용하여 메모리 사용량을 얻은 결과는 다음과 같습니다.
재시도 버튼을 클릭하면 점점 더 많은 메모리가 할당되는 것을 볼 수 있습니다. 이는 이전에 할당된 메모리가 해제되지 않았음을 의미합니다. 타이머는 교체되지 않고 계속 작동 중입니다.
이 문제를 해결하는 방법? setInterval
의 반환 값은 이 간격을 취소하는 데 사용할 수 있는 간격 ID입니다. 이 특별한 경우에는 구성 요소가 언로드된 후 clearInterval
호출할 수 있습니다.
useEffect(() => { const IntervalId = setInterval(() => { if (currentCicles.current >= cicles) { onFinish(); 반품; } currentCicles.current++; }, 500); return () => ClearInterval(intervalId); }, [])
때로는 코드를 작성할 때 이 문제를 찾기가 어렵습니다. 가장 좋은 방법은 구성 요소를 추상화하는 것입니다.
여기에서 React를 사용하면 이 모든 로직을 사용자 정의 Hook로 래핑할 수 있습니다.
'반응'에서 { useEffect }를 가져옵니다. const useTimeout = (refreshCycle = 100, 콜백) => { useEffect(() => { if (refreshCycle <= 0) { setTimeout(콜백, 0); 반품; } const IntervalId = setInterval(() => { 콜백(); }, 새로고침주기); return () => ClearInterval(intervalId); }, [refreshCycle, setInterval,clearInterval]); }; 기본 useTimeout 내보내기;
이제 setInterval
사용해야 할 때마다 다음을 수행할 수 있습니다.
const handlerTimeout = () => ...; useTimeout(100, handlerTimeout);
이제 추상화의 이점이기도 한 메모리 누수에 대한 걱정 없이 이 useTimeout Hook
사용할 수 있습니다.
Web API는 수많은 이벤트 리스너를 제공합니다. 앞서 우리는 setTimeout
대해 논의했습니다. 이제 addEventListener
를 살펴보겠습니다.
이 예에서는 키보드 단축키 기능을 만듭니다. 페이지마다 기능이 다르기 때문에 다양한 단축키 기능이 생성됩니다.
함수 homeShortcuts({ 키}) { if (키 === 'E') { console.log('위젯 편집') } } // 사용자가 홈페이지에 로그인하면 document.addEventListener('keyup', homeShortcuts)를 실행합니다. // 사용자가 작업을 수행한 다음 설정 함수로 이동합니다. settingsShortcuts({ key}) { if (키 === 'E') { console.log('설정 편집') } } // 사용자가 홈페이지에 로그인하면 document.addEventListener('keyup', settingsShortcuts)를 실행합니다.
두 번째 addEventListener
실행할 때 이전 keyup
이 정리되지 않는다는 점을 제외하면 여전히 괜찮아 보입니다. keyup
리스너를 바꾸는 대신 이 코드는 또 다른 callback
추가합니다. 즉, 키를 누르면 두 가지 기능이 실행됩니다.
이전 콜백을 지우려면 removeEventListener
사용해야 합니다.
document.removeEventListener('keyup', homeShortcuts);
위 코드를 리팩터링합니다.
함수 homeShortcuts({ 키}) { if (키 === 'E') { console.log('위젯 편집') } } // 사용자가 집에 도착하면 실행합니다. document.addEventListener('keyup', homeShortcuts); // 사용자가 몇 가지 작업을 수행하고 설정으로 이동합니다. 함수 설정단축키({ 키}) { if (키 === 'E') { console.log('설정 편집') } } // 사용자가 집에 도착하면 실행합니다. document.removeEventListener('keyup', homeShortcuts); document.addEventListener('keyup', settingsShortcuts);
경험상 전역 개체의 도구를 사용할 때는 매우 주의해야 합니다.
관찰자는 많은 개발자가 인식하지 못하는 브라우저 웹 API 기능입니다. 이는 HTML 요소의 가시성 또는 크기 변경을 확인하려는 경우에 강력합니다.
IntersectionObserver
인터페이스(Intersection Observer API의 일부)는 대상 요소와 상위 요소 또는 최상위 문서 viewport
의 교차 상태를 비동기적으로 관찰하는 방법을 제공합니다. 상위 요소와 viewport
root
라고 합니다.
강력하기는 하지만 주의해서 사용해야 합니다. 물체 관찰을 마친 후에는 사용하지 않을 때 취소하는 것을 잊지 마세요.
코드를 살펴보세요:
const 참조 = ... const visible = (표시) => { console.log(`${visible}`); } useEffect(() => { 만약 (!ref) { 반품; } 관찰자.현재 = 새로운 IntersectionObserver( (항목) => { if (!entries[0].isIntersecting) { 보이는(사실); } 또 다른 { 가시적(거짓); } }, { rootMargin: `-${header.height}px` }, ); 관찰자.current.observe(ref); }, [참조]);
위의 코드는 괜찮아 보입니다. 그러나 구성 요소가 언로드되면 관찰자는 어떻게 되나요? 지워지지 않고 메모리가 누출됩니다. 이 문제를 어떻게 해결하나요? disconnect
방법을 사용하면 됩니다.
const 참조 = ... const visible = (표시) => { console.log(`${visible}`); } useEffect(() => { 만약 (!ref) { 반품; } 관찰자.현재 = 새로운 IntersectionObserver( (항목) => { if (!entries[0].isIntersecting) { 보이는(사실); } 또 다른 { 가시적(거짓); } }, { rootMargin: `-${header.height}px` }, ); 관찰자.current.observe(ref); return () =>observer.current?.disconnect(); }, [참조]);
Window에 객체를 추가하는 것은 흔한 실수입니다. 일부 시나리오에서는 특히 창 실행 컨텍스트에서 this
키워드를 사용할 때 찾기 어려울 수 있습니다. 다음 예를 살펴보십시오.
함수 addElement(요소) { if (!this.stack) { this.스택 = { 요소: [] } } this.stack.elements.push(요소); }
무해해 보이지만 addElement
호출하는 컨텍스트에 따라 다릅니다. Window Context에서 addElement를 호출하면 힙이 커집니다.
또 다른 문제는 전역 변수를 잘못 정의하는 것입니다.
var a = 'example 1'; // 범위는 var가 생성된 위치로 제한됩니다. b = 'example 2' // Window 객체에 추가됩니다.
이 문제를 방지하려면 엄격 모드를 사용하면 됩니다.
"엄격하게 사용하세요"
엄격 모드를 사용하면 이러한 동작으로부터 자신을 보호하겠다는 신호를 JavaScript 컴파일러에 보냅니다. 필요할 때 여전히 Window를 사용할 수 있습니다. 그러나 명시적인 방법으로 사용해야 합니다.
엄격 모드가 이전 예에 어떤 영향을 미치는지:
addElement
함수의 경우 전역 범위에서 호출되면 정의되지 this
.
변수에 const | let | var
지정하지 않으면 다음 오류가 발생합니다.
잡히지 않은 ReferenceError: b가 정의되지 않았습니다.
DOM 노드도 메모리 누수로부터 면역되지 않습니다. 우리는 이에 대한 참조를 저장하지 않도록 주의해야 합니다. 그렇지 않으면 여전히 액세스할 수 있기 때문에 가비지 수집기가 이를 정리할 수 없습니다.
작은 코드 조각으로 이를 보여줍니다.
const 요소 = []; const list = document.getElementById('list'); 함수 addElement() { // 노드 정리 list.innerHTML = ''; const pElement= document.createElement('p'); const element = document.createTextNode(`${elements.length} 요소 추가`); pElement.appendChild(요소); list.appendChild(pElement); elements.push(pElement); } document.getElementById('addElement').onclick = addElement;
addElement
함수는 목록 p
지우고 여기에 새 요소를 하위 요소로 추가합니다. 새로 생성된 이 요소는 elements
배열에 추가됩니다.
다음에 addElement
실행되면 해당 요소는 p
목록에서 제거되지만 elements
배열에 저장되므로 가비지 수집에 적합하지 않습니다.
함수를 몇 번 실행한 후 모니터링합니다.
위 스크린샷에서 노드가 어떻게 손상되었는지 확인하세요. 그렇다면 이 문제를 해결하는 방법은 무엇입니까? elements
배열을 지우면 가비지 수집 대상이 됩니다.
이 글에서는 메모리 누수의 가장 일반적인 방법을 살펴보았습니다. JavaScript 자체가 메모리 누수를 일으키지 않는다는 것은 분명합니다. 대신 개발자 측의 의도하지 않은 메모리 보유로 인해 발생합니다. 코드가 깨끗하고 우리가 스스로 정리하는 것을 잊지 않는 한 누출은 발생하지 않습니다.
JavaScript에서 메모리와 가비지 수집이 어떻게 작동하는지 이해하는 것은 필수입니다. 일부 개발자는 자동이므로 이 문제에 대해 걱정할 필요가 없다는 잘못된 인식을 갖습니다.