
Codefug Blog

리액트 과거, 현재, 미래
애플리케이션에서 발생하는 버그와 디버깅 이슈는 리액트 밖에서 일어날 때도 있습니다.
자바스크립트 메모리, 네트워크, 소스, 실제 HTML 및 CSS 등 리액트 외부 환경을 디버깅하려면 브라우저 개발자 도구가 필요합니다.

위 사진처럼 dev tool은 해당 페이지의 다양한 정보를 보여줍니다.
개발자 도구는 시크릿 모드로 사용하는 것을 권장합니다.

확장 프로그램이 전역 변수를 저장할 수 있기 때문입니다. 시크릿 모드에서는

시크릿 모드에서 사용 가능한 확장 프로그램이 없다면 확장 프로그램이 저장하는 전역 변수가 존재하지 않습니다.
현재 웹페이지를 구성하는 HTML, CSS 등의 정보를 확인할 수 있습니다.

현재 웹페이지를 구성하는 HTML을 나타냅니다.

요소를 추가하거나 삭제, 수정할 수 있고 중단점을 이용해서 변화를 일으킨 코드를 추적할 수 있습니다.


해당 부분에서 php를 사용했고 이와 관련된 코드가 무엇인지 확인할 수 있습니다.

Styles: 요소와 관련된 스타일 정보 확인
Computed: 패딩, 보더, 마진 등 CSS 적용 결과값을 확인할 수 있습니다. 최종적으로 어떤 결과물인지 확인합니다.
Layout: CSS 그리드나 레이아웃 관련 정보를 확인합니다.
Event listeners: 이벤트 리스너들을 확인합니다. Ancestor를 해제하면 해당 요소에 지정된 이벤트만 볼 수 있습니다.
DOM Breakpoints: 중단점이 있는지 알려주는 탭입니다.
Properties: 해당 요소가 가진 모든 속성값을 표시합니다. js의 .attributes와의 차이는 지정된 속성뿐만 아니라 모든 속성값이 나온다는 것입니다.
Accessibility: 웹 이용에 어려움을 겪는 장애인, 노약자를 위한 스크린 리더기 등이 활용하는 값입니다.
웹 애플리케이션을 불러오기 위해 실행하거나 참조된 모든 파일을 확인할 수 있습니다. JS, CSS, HTML, Font 등 여러 파일 정보를 확인할 수 있습니다.

inflearn 사이트에서 개발자 도구 > ctrl+p를 누른 모습입니다. 파일에 들어가면 실제 코드 내용을 확인할 수 있습니다.
프로덕션 모드에서는 파일이 압축되어 있어 확인하기 어렵지만 개발 모드에서는 리액트 코드도 확인할 수 있습니다.

실제 코드처럼 중단점을 찍은 후 디버깅할 수 있습니다.


Source의 오른쪽에는 위와 같은 정보가 있습니다.
Watch: 감시하고 싶은 변수를 선언하고, 해당 변수의 정보를 확인할 수 있는 메뉴입니다.
Breakpoints: 현재 웹사이트에서 추가한 중단점을 확인합니다. 소스 탭에서 추가한 모든 중단점을 확인할 수 있습니다.
Scope: 로컬 스코프, 클로저, 전역 스코프 등을 확인합니다.
Call Stack: 현재 중단점의 콜스택을 확인합니다.
Global Listeners: 전역 스코프에 추가된 리스너 목록입니다.
XHR/fetch Breakpoints, DOM Breakpoints, Event Listener Breakpoints
CSP Violation Breakpoints: 다양한 종류의 중단점을 확인합니다.
해당 페이지에 접속하는 순간부터 발생하는 모든 네트워크 관련 작동이 기록됩니다. 외부 데이터와 통신하는 정보가 모두 들어가 있습니다.

3번째 줄에 Fetch/XHR, Doc 등으로 태그를 만들어서 네트워크 요청의 종류를 필터링할 수 있습니다.

특정 네트워크의 상태를 확인할 수도 있습니다.
개발자 모드로 실행되면 웹 소켓을 통해 핫 리로딩됩니다.

하단에는 페이지를 불러오는 기간 동안 발생한 request의 건수, 다운로드한 리소스의 크기를 확인할 수 있습니다.
모바일의 경우 리소스의 크기만큼 모바일 네트워크 비용을 지불해야 하고 속도에도 영향을 미치기 때문에 건수와 크기를 최적화할 필요가 있습니다.
gzip이나 brotli를 활용해서 리소스를 압축하거나 이미지 최적화를 할 수 있습니다.

왼쪽 위 설정을 누르고 Screenshots를 누르면 웹페이지 로딩을 네트워크 요청 흐름에 따라 확인할 수 있습니다.

시간별로 웹페이지를 스크린샷처럼 찍어서 보여줍니다.
메모리 관련 정보를 확인합니다. 애플리케이션에서 발생하는 메모리 누수, 속도 저하, 웹페이지 프리징 현상을 확인할 수 있는 도구입니다.

React-dev-tool과 비슷하게 프로파일링 작업을 거쳐야 원하는 정보를 볼 수 있습니다.
위에서부터 다음과 같은 프로파일링 종류입니다.
Heap snapshot: 메모리 상황을 사진 찍듯이 촬영할 수 있습니다.
Allocation instrumentation of timeline: 시간의 흐름에 따라 메모리의 변화를 볼 수 있습니다. 로딩 과정의 메모리 변화, 상호작용했을 때 메모리의 변화 과정을 확인합니다.
Allocation sampling: 메모리 공간을 차지하고 있는 자바스크립트 함수를 확인합니다.

디버깅하고 있는 JS VM 환경을 선택해서 실제 해당 페이지가 자바스크립트 힙을 얼마나 점유하고 있는지 확인할 수 있습니다.

Google의 예시입니다. JS 실행에 따라 실시간으로 크기가 변하며 이 크기만큼 브라우저에 부담을 주기 때문에 눈여겨 볼만합니다.
현재 페이지의 메모리 상태를 확인할 수 있는 메모리 프로파일 도구입니다.
const DUMMY_LIST = []
export default function App(){
function handleClick(){
Array.from({ length: 10_000_000 }).forEach((_,idx)=>
DUMMY_LIST.push(Math.random()*idx);
)
}
alert('complete');
return <button onClick={handleClick}>BUG</button>
}//컴포넌트 외부에 있는 배열에 천만 개의 랜덤한 값을 push하는 컴포넌트

누르기 전 메모리 모습입니다.

누른 후의 메모리 모습입니다. 세 번째 줄 array에 엄청난 크기가 들어간 것을 확인할 수 있습니다.

Objects allocated between snapshot 1 and snapshot 2를 누르면

둘의 교집합만 나옵니다. array의 토글을 열면 그 안의 객체들을 확인할 수 있습니다.

위처럼 전역 객체로 지정하면 어떤 값이 들어있는지도 확인할 수 있습니다.

window의 temp1에 저장됩니다. shallow size와 Retained Size는 참조값을 포함하느냐 아니냐의 차이입니다.
function Y() {
this.j = 5;
}
function X() {
this.x = 3;
this.y = new Y();
}
export default function App() {
function handleClick() {
instances.push(new X());
}
return <button onClick={handleClick}>+</button>;
}
이때 X는 52 100, Y는 48 48의 Shallow Size, Retained Size를 갖습니다.
X는 Y를 참조하고 있으나 Y는 변수만 갖고 있기 때문에 X만 참조 값을 포함해서 Retained Size가 100입니다.
메모리 누수가 의심될 경우 Retained Size와 Shallow Size의 차이가 큰 객체를 찾는 것이 좋습니다. (다수의 객체를 참조하고 있어 누수 위험이 높습니다)
이번엔 useCallback으로 재할당되는지 여부를 디버깅으로 확인해보겠습니다.
function Component({ number }: { number: number }) {
const callbackHandleClick = useCallback(() => {
console.log(number);
}, [number]);
const noCallbackHandleClick = () => {
console.log(number);
};
return (
<>
<button onClick={callbackHandleClick}>call</button>
<button onClick={noCallbackHandleClick}>no</button>
</>
);
}
function App(){
const [toggle, setToggle] = useState(false);
//...
return (<>
<button onClick={()=>{setToggle(()=>!toggle)}}>재렌더링</button>
<Component number={5} />
</>)
}
내려주는 number props는 고정이지만 부모 컴포넌트인 App이 재렌더링됨에 따라 자식 컴포넌트인 Component도 재렌더링됩니다. 이때 useCallback을 사용한 callbackHandleClick은 재호출되지 않는 것을 확인해보겠습니다.

이전과 이후의 snap shot을 비교해보면 위와 같습니다. callbackHandleClick()은 없고 noCallbakHandleClick()만 존재하는 것을 확인할 수 있습니다.
아래 () @163297은 익명 함수입니다. (setToggle의 콜백)
setToggle(function toggle() {
return !toggle;
});
기명 함수로 작성하면 다음과 같이 됩니다.

시간의 흐름에 따라 메모리 변화를 기록합니다.
구간을 정해서 어떤 변화가 있었는지 그 객체를 확인할 수 있습니다.

전역 변수로 지정도 할 수 있습니다.


시간의 흐름에 따라 발생하는 메모리 점유를 확인하는데 JS 실행 스택별로 분석할 수 있고 이 분석을 함수 단위로 합니다.
다음 코드를 예시로 살펴보겠습니다.
const DUMMY: Array<string> = [];
function App() {
const [toggle, setToggle] = useState(false);
return (
// ...
<button
onClick={() => {
Array.from({ length: 3000 }).forEach(function addDummy() {
DUMMY.push(Math.random().toString());
});
setToggle(function toggle(prev) {
return !prev;
});
}}
>
)
버튼을 누르면 DUMMY에 3000개의 데이터가 random().toString()을 거쳐서 들어갑니다.
sampling을 돌리면 toString이 가장 많이 차지한 것을 확인할 수 있습니다.

맨 오른쪽 파일 경로로 소스코드에 접근할 수도 있습니다.
클라이언트 사이드의 메모리 누수는 디바이스에 따라 다르고 그 영향은 해당 디바이스에만 닿지만 서버 사이드의 메모리 누수는 서버 자체에 부담을 줘서 모든 서비스에 영향을 줍니다.
dev tool로 서버단도 디버깅이 가능합니다.
// package.json
{"scripts": { "dev" : NODE_OPTIONS='--inspect' next dev }}
npm run dev
여러가지 나오고
이후 web socket 주소가 나옴
https://nextjs.org/docs/pages/building-your-application/configuring/debugging
cross-env를 설치한 후에 https://www.npmjs.com/package/cross-env
{ "scripts": { "dev": "cross-env NODE_OPTIONS='--inspect' next dev" } }
이렇게 해야 실행됩니다.

chrome://inspect로 이동하면 위와 같은 화면이 나옵니다.

Open dedicated DevTools for Node를 클릭합니다.

이제 해당 VM 인스턴스 (node 서버 환경)를 활용해서 디버깅할 수 있습니다.
가상 시나리오를 세워서 서버에 트래픽을 발생시켜 확인해보겠습니다.
ab는 웹서버 성능 검사 도구로 HTTP 서버의 성능을 벤치마킹할 수 있는 오픈소스 도구입니다.
ab -k -c 50 -n 10000 "http://127.0.0.1:3000/"
https://stackoverflow.com/questions/49893994/apachebench-installation-on-windows-10
위 커맨드로 해당 url을 향해 50개의 요청을 10,000회 보낼 수 있습니다.
똑같이 서버단도 디버깅하면 메모리가 증가하는 것을 확인할 수 있습니다.
SSR에서 메모리가 누수되면 서버에서 계속 쌓여서 과부하가 오기 때문에 순수함수로 구현해야 합니다.