<StrictMode>를 사용하면 컴포넌트에서 흔히 발생하는 버그를 개발 초기에 발견할 수 있음.
<StrictMode>
<App />
</StrictMode>
<StrictMode>내부 컴포넌트 트리에 대한 추가 개발 동작 및 경고를 활성화하려면 StrictMode를 사용할 것:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
Strict Mode는 다음과 같은 개발 전용 동작을 활성화함:
props를 받지 않음.
<StrictMode>로 래핑된 트리 내부에서는 Strict Mode를 해제할 방법이 없음. 이 때문에 <StrictMode> 내의 모든 컴포넌트가 검사된다는 확신을 가질 수 있음. 제품을 작업하는 두 팀이 검사의 가치에 대해 의견이 다를 경우, 합의를 도출하거나 <StrictMode>를 트리에서 아래로 이동해야 함.
Strict Mode를 사용하면 <StrictMode> 컴포넌트 내부의 전체 컴포넌트 트리에 대해 개발 전용 검사를 추가로 수행할 수 있음. 이러한 검사를 통해 개발 프로세스 초기에 컴포넌트에서 흔히 발생하는 버그를 발견할 수 있음.
전체 앱에서 Strict Mode를 사용하려면 루트 컴포넌트를 렌더링할 때 <StrictMode>로 감싸면 됨:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
특히 새로 만든 앱의 경우 전체 앱을 Strict Mode로 감싸는 것이 좋음. createRoot를 대신 호출하는 프레임워크를 사용하는 경우, 해당 프레임워크의 문서를 참조하여 Strict Mode를 활성화하는 방법을 확인할 것.
Strict Mode 검사는 개발 중에만 실행되지만, 코드에 이미 존재하지만 프로덕션 환경에서 안정적으로 재현하기 어려운 버그를 찾는 데 도움이 됨. Strict Mode를 사용하면 사용자가 버그를 신고하기 전에 버그를 수정할 수 있음.
Note
Strict Mode에서는 개발 단계에서 다음과 같은 검사가 가능함:
- 불완전한 렌더링으로 인한 버그를 찾기 위해 컴포넌트가 한 번 더 재렌더링함.
- Effect cleanup이 누락되어 발생한 버그를 찾기 위해 컴포넌트가 Effects를 한 번 더 재실행함.
- 컴포넌트가 더 이상 사용되지 않는 API를 사용하는지 확인함.
이러한 모든 검사는 개발 전용이며 프로덕션 빌드에는 영향을 미치지 않음.
애플리케이션의 모든 부분에 대해 Strict Mode를 활성화할 수 있음:
import { StrictMode } from 'react';
function App() {
return (
<>
<Header />
<StrictMode>
<main>
<Sidebar />
<Content />
</main>
</StrictMode>
<Footer />
</>
);
}
이 예제에서는 Header 및 Footer 컴포넌트에 대해 Strict Mode 검사가 실행되지 않음. 그러나 Sidebar 및 Content는 물론 그 안에 있는 모든 컴포넌트에는 아무리 깊어도 Strict Mode 검사가 실행됨.
React는 모든 컴포넌트가 순수한 함수라고 가정함. 즉, React 컴포넌트는 동일한 입력(props, state, context)이 주어지면 항상 동일한 JSX를 반환해야 함.
이 규칙을 위반하는 컴포넌트는 예측할 수 없게 동작하며 버그를 유발함. 의도치 않게 비순수한 코드를 찾을 수 있도록 Strict Mode는 개발 과정에서 일부 함수(순수해야 하는 함수만)를 두 번 호출함. 예를 들면:
useState, set functions, useMemo 또는 useReducer에 전달하는 함수constructor, render, shouldComponentUpdate와 같은 일부 클래스 컴포넌트 메서드 (참고: 전체 목록)순수한 함수는 매번 동일한 결과를 생성하기 때문에 함수를 두 번 실행해도 동작이 변경되지 않음. 그러나 함수가 비순수한 경우(예: 받은 데이터를 변경하는 경우) 두 번 실행하면 눈에 띄는 차이가 있으므로(그래서 비순수한 것!) 버그를 조기에 발견하고 수정하는 데 도움이 됨.
다음은 Strict Mode에서 이중 렌더링이 버그를 조기에 발견하는 데 어떻게 도움이 되는지 설명하는 예시임.
이 StoryTray 컴포넌트는 여러 개의 stories 배열을 가져와 "Create Story" 항목을 배열의 끝에 추가함:
// StoryTray.js
export default function StoryTray({ stories }) {
const items = stories;
items.push({ id: 'create', label: 'Create Story' });
return (
<ul>
{items.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
위 코드에는 실수가 있음. 그러나 초기 출력은 올바르게 보이기 때문에 놓치기 쉬움.
이 실수는 StoryTray 컴포넌트가 여러 번 다시 렌더링하면 더욱 눈에 띄게 됨. 예를 들어 StoryTray를 마우스로 가리킬 때마다 다른 배경색으로 다시 렌더링하면:
// StoryTray.js
import { useState } from 'react';
export default function StoryTray({ stories }) {
const [isHover, setIsHover] = useState(false);
const items = stories;
items.push({ id: 'create', label: 'Create Story' });
return (
<ul
onPointerEnter={() => setIsHover(true)}
onPointerLeave={() => setIsHover(false)}
style={{
backgroundColor: isHover ? '#ddd' : '#fff'
}}
>
{items.map(story => (
<li key={story.id}>
{story.label}
</li>
))}
</ul>
);
}
StoryTray 컴포넌트 위로 마우스를 가져갈 때마다 'Create Story'가 목록에 다시 추가되는 것을 볼 수 있음. 코드의 의도는 마지막에 한 번만 추가하는 것이었지만, StoryTray는 props의 stories 배열을 직접 수정함. StoryTray는 렌더링할 때마다 동일한 배열의 끝에 "Create Story"를 다시 추가함. 즉, StoryTray는 순수한 함수가 아니기 때문에 여러 번 실행하면 다른 결과가 생성됨.
이 문제를 해결하려면 배열의 복사본을 만든 다음 원본이 아닌 복사본을 수정하면 됨:
export default function StoryTray({ stories }) {
const items = stories.slice(); // Clone the array
// ✅ Good: Pushing into a new array
items.push({ id: 'create', label: 'Create Story' });
이렇게 하면 StoryTray 함수가 순수해짐. 이 함수가 호출될 때마다 배열의 새 복사본만 수정하고 외부 객체나 변수에 영향을 주지 않음. 이렇게 하면 버그가 해결되지만, 컴포넌트의 동작에 문제가 있다는 것이 명백해지기 전에 컴포넌트를 더 자주 재렌더링해야 했음.
원래 예제에서는 버그가 명백하지 않았음. 원래의 (버그가 있는) 코드를 <StrictMode>로 감싸면?
Strict Mode에서는 항상 렌더링 함수를 두 번 호출하므로 실수를 바로 확인할 수 있음("Create Story"가 두 번 표시됨). 따라서 프로세스 초기에 이러한 실수를 발견할 수 있음. 컴포넌트를 Strict Mode에서 렌더링하도록 수정하면, 이전의 호버 기능과 같이 향후 발생할 수 있는 많은 프로덕션 버그도 수정할 수 있음.
Strict Mode가 없으면 리렌더를 더 추가하기 전까지는 버그를 놓치기 쉬웠음. Strict Mode를 사용하면 동일한 버그를 바로 발견할 수 있음. Strict Mode를 사용하면 팀과 사용자에게 푸시하기 전에 버그를 발견할 수 있음.
Note
React 개발자 도구를 설치한 경우 두 번째 렌더링 호출 중의 모든
console.log호출이 약간 흐리게 표시됨. React 개발자 도구는 이를 완전히 막는 설정(기본값은 off)도 제공함.
Strict Mode는 Effects의 버그를 찾는 데도 도움이 될 수 있음.
모든 Effect에는 setup 코드가 있고 cleanup 코드가 있을 수 있음. 일반적으로 React는 컴포넌트가 mount될 때(화면에 추가될 때) setup을 호출하고 컴포넌트가 unmount될 때(화면에서 제거될 때) cleanup을 호출함. 그런 다음 React는 마지막 렌더링 이후 dependencies가 변경된 경우 cleanup과 setup을 다시 호출함.
Strict Mode가 켜져 있으면 React는 모든 Effect에 대해 개발 단계에서 setup + cleanup 사이클을 한 번 더 실행함. 이는 의외로 느껴질 수 있지만 수동으로 잡기 어려운 미묘한 버그를 발견하는 데 도움이 됨.
컴포넌트를 채팅에 연결하는 다음 예시는 Strict Mode에서 Effects를 다시 실행하는 것이 버그를 조기에 발견하는 데 어떻게 도움이 되는지 보여줌:
// App.js
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
const roomId = 'general';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
}, []);
return <h1>Welcome to the {roomId} room!</h1>;
}
// chat.js
let connections = 0;
export function createConnection(serverUrl, roomId) {
// A real implementation would actually connect to the server
return {
connect() {
console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
connections++;
console.log('Active connections: ' + connections);
},
disconnect() {
console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
connections--;
console.log('Active connections: ' + connections);
}
};
}
이 코드에는 문제가 있지만 즉시 알아차리기 어려움.
문제를 더 명확하게 파악하기 위해 기능을 구현할 것. 아래 예제에서는 roomId가 하드코딩되어 있지 않은 대신, 사용자가 드롭다운에서 연결하려는 roomId를 선택할 수 있음. 'Open chat'를 클릭한 다음 다른 채팅방을 하나씩 선택하면 콘솔에서 활성화된 커넥션의 개수를 추적함:
// App.js
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
}, [roomId]);
return <h1>Welcome to the {roomId} room!</h1>;
}
export default function App() {
const [roomId, setRoomId] = useState('general');
const [show, setShow] = useState(false);
return (
<>
<label>
Choose the chat room:{' '}
<select
value={roomId}
onChange={e => setRoomId(e.target.value)}
>
<option value="general">general</option>
<option value="travel">travel</option>
<option value="music">music</option>
</select>
</label>
<button onClick={() => setShow(!show)}>
{show ? 'Close chat' : 'Open chat'}
</button>
{show && <hr />}
{show && <ChatRoom roomId={roomId} />}
</>
);
}
열려있는 커넥션의 개수가 계속 증가하는 것을 알 수 있음. 실제 앱에서는 성능 및 네트워크 문제가 발생할 수 있음. 문제는 Effect에 cleanup 함수가 없다는 것:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
이제 Effect가 스스로 "clean up"하고 오래된 커넥션을 삭제하므로 누수 문제가 해결되었음. 하지만 더 많은 기능(드롭다운)을 추가하기 전까지는 문제가 보이지 않았음.
원래 예제에서는 버그가 명백하지 않았음. 원래의 (버그가 있는) 코드를 <StrictMode>로 감싸면?
Strict Mode를 사용하면 문제가 있음을 즉시 알 수 있음(활성화된 커넥션 수가 2로 증가하므로). Strict Mode는 모든 Effects에 대해 추가 setup+cleanup 사이클을 실행함. 이 Effects에는 cleanup 로직이 없으므로 추가 커넥션을 생성하지만 파괴하지는 않음. 이것은 cleanup 함수가 누락되었다는 힌트임.
Strict Mode를 사용하면 이러한 실수를 프로세스 초기에 발견할 수 있음. Strict Mode에서 cleanup 함수를 추가하여 Effects를 수정하면 향후 발생할 수 있는 많은 프로덕션 버그도 수정할 수 있음.
Strict Mode가 없으면 Effects에 cleanup이 필요하다는 사실을 놓치기 쉬웠음. 개발 단계에서 Effects를 setup 대신 setup → cleanup → setup을 실행하면 누락된 cleanup 로직이 더 눈에 띄게 됨.
React는 <StrictMode> 트리 내의 일부 컴포넌트가 더 이상 사용되지 않는 다음 API 중 하나를 사용하는 경우 경고를 표시함:
findDOMNode (참고: 대안)UNSAFE_componentWillMount와 같은 UNSAFE_ 클래스 생명주기 메서드 (참고: 대안)childContextTypes, contextTypes 및 getChildContext). (참고: 대안)this.refs). (참고: 대안)이러한 API는 주로 과거의 클래스 컴포넌트에서 사용되므로 최신 앱에서는 거의 나타나지 않음.