“React 앱은 어떻게 빨라지고, 정확해지는가?”
프론트엔드 개발을 하다 보면 느린 앱, 의도치 않은 리렌더링, 테스트되지 않은 코드 등 여러 실무 이슈를 만나게 됩니다. 이 글에서는 다음과 같은 내용을 중심으로 정리해 봅니다:
자바스크립트 코드는 그냥 위에서 아래로 실행되는 것처럼 보이지만, 사실 먼저 메모리 할당이 먼저 일어나는 단계가 있습니다. 이를 실행 컨텍스트라고 합니다.
예를 들어 다음과 같은 코드가 있을 때:
console.log(a); // 👉 undefined
var a = 10;
코드는 두 단계로 처리됩니다:
• Creation Phase (생성 단계): 자바스크립트가 코드를 실행하기 전에, 먼저 변수나 함수 선언만 따로 기억해두는 단계
• Execution Phase (실행 단계): 이후에 10이라는 값이 대입됩니다.
let과 const는 이 과정에서 TDZ(Temporal Dead Zone)에 들어가서, 선언 전에 접근하면 오류가 납니다.
이 차이를 알고 있으면 디버깅할 때 훨씬 유리합니다.
console.log(b); // ❌ ReferenceError
let b = 10;
✅ TDZ
Temporal Dead Zone (일시적 사각지대)는 자바스크립트에서 변수가 선언되었지만 초기화되기 전까지 접근할 수 없는 상태
function outer() {
const name = "sun";
return function inner() {
console.log(name);
};
}
const say = outer();
say(); // "sun"
이 코드에서 inner 함수는 outer 함수가 끝난 뒤에도 name 변수를 기억하고 있습니다. 이게 바로 클로저(Closure) 입니다. 이 구조는 React의 상태 관리(setState)나 이벤트 핸들러처럼 “과거의 값을 기억해야 하는 상황”에 자주 쓰입니다.
console.dir(say)을 브라우저에서 실행하면, 함수 내부의 Scopes
를 통해 실제 클로저 환경을 확인할 수 있습니다.
컴포넌트가 불필요하게 리렌더링되면 사용자 체감 속도가 느려지고 성능이 저하됩니다.
이걸 방지하는 대표적인 방법이 React.memo, useCallback, useMemo입니다.
const Child = React.memo(({ onClick }) => {
console.log('Child rendered');
return <button onClick={onClick}>+</button>;
});
const Parent = () => {
const [count, setCount] = useState(0);
const clickHandler = useCallback(() => {
console.log('clicked');
}, []);
return (
<>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(count + 1)}>카운트+</button>
<Child onClick={clickHandler} />
</>
);
};
초기 번들 크기가 크면 LCP(Largest Contentful Paint) 지표가 나빠집니다.
이걸 해결하는 대표적인 방법이 React.lazy + Suspense입니다.
const Chart = React.lazy(() => import('./Chart'));
function Dashboard() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<Chart />
</Suspense>
);
}
이렇게 하면 Chart 컴포넌트는 처음부터 로딩되지 않고, Dashboard에서 호출될 때만 다운로드되기 때문에 초기 로딩 속도가 빨라집니다.
React에서는 @testing-library/react + Jest 조합으로 단위 테스트를 작성합니다.
예를 들어 카운터 테스트는 이렇게 작성할 수 있습니다:
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';
test('버튼을 누르면 카운트가 1 증가해야 한다', () => {
render(<Counter />);
expect(screen.getByTestId('count')).toHaveTextContent('현재 숫자: 0');
fireEvent.click(screen.getByText('+'));
expect(screen.getByTestId('count')).toHaveTextContent('현재 숫자: 1');
});
유형 | 목적 | 대표 도구 |
---|---|---|
Unit Test | 함수/컴포넌트 단위 테스트 | Jest |
Integration | 컴포넌트 간 상호작용 테스트 | React Testing Library |
E2E (End-to-End) | 전체 흐름, 브라우저 테스트 | Cypress, Playwright |
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('renders button with text', () => {
render(<Button text="Click me" />);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
성능은 눈에 보이는 속도, 테스트는 눈에 보이지 않는 정확성입니다.
이 둘을 모두 잡기 위해서는 JS의 실행 구조를 이해하고, React 최적화를 알고, 테스트를 습관처럼 작성해야 합니다.
그 시작은 콘솔에 console.dir(fn) 찍어보는 것부터입니다.