<StrictMode><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는 다음과 같은 개발 전용 동작들을 활성화해요:
StrictMode는 props를 받지 않아요.
<StrictMode>로 감싸진 트리 내부에서 Strict Mode를 해제할 방법은 없어요. 이를 통해 <StrictMode> 내부의 모든 컴포넌트가 체크된다는 확신을 가질 수 있어요. 제품을 개발하는 두 팀이 체크가 가치 있는지에 대해 의견이 다르다면, 합의에 도달하거나 트리에서 <StrictMode>를 아래로 이동시켜야 해요.부연 설명: 예를 들어, A 팀은 Strict Mode가 필요하다고 생각하고 B 팀은 불필요하다고 생각한다면, 전체 앱에 적용하는 대신 A 팀이 담당하는 컴포넌트 부분만 감싸서 사용할 수 있어요.
Strict Mode는 <StrictMode> 컴포넌트 내부의 전체 컴포넌트 트리에 대해 개발 전용 추가 체크를 활성화해요. 이러한 체크는 개발 프로세스 초기에 컴포넌트의 흔한 버그를 찾는 데 도움을 줘요.
전체 앱에 Strict Mode를 활성화하려면, 루트 컴포넌트를 렌더링할 때 <StrictMode>로 감싸주세요:
// {6,8}
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를 사용하면 사용자가 보고하기 전에 버그를 수정할 수 있어요.
부연 설명: 프로덕션 빌드에서는 이런 체크들이 전혀 실행되지 않아요. 그래서 성능에 영향을 주지 않고 개발 중에만 버그를 잡아낼 수 있는 거죠!
참고
Strict Mode는 개발 환경에서 다음 체크들을 활성화해요:
- 불순한(impure) 렌더링으로 인한 버그를 찾기 위해 컴포넌트가 한 번 더 리렌더링돼요.
- Effect 클린업 누락으로 인한 버그를 찾기 위해 컴포넌트가 Effect를 한 번 더 재실행해요.
- ref 클린업 누락으로 인한 버그를 찾기 위해 컴포넌트가 ref 콜백을 한 번 더 재실행해요.
- 컴포넌트에서 더 이상 사용되지 않는 API의 사용 여부를 체크해요.
이 모든 체크는 개발 전용이며 프로덕션 빌드에는 영향을 주지 않아요.
애플리케이션의 특정 부분에만 Strict Mode를 활성화할 수도 있어요:
// {7,12}
import { StrictMode } from 'react';
function App() {
return (
<>
<Header />
<StrictMode>
<main>
<Sidebar />
<Content />
</main>
</StrictMode>
<Footer />
</>
);
}
이 예제에서는 Header와 Footer 컴포넌트에 대해서는 Strict Mode 체크가 실행되지 않아요. 하지만 Sidebar와 Content, 그리고 그 내부의 모든 컴포넌트들은 아무리 깊이 중첩되어 있어도 체크가 실행돼요.
참고
앱의 일부에만
StrictMode가 활성화되면, React는 프로덕션에서 가능한 동작만 활성화해요. 예를 들어, 앱의 루트에서<StrictMode>가 활성화되지 않았다면, 초기 마운트 시 Effect를 한 번 더 재실행하지 않아요. 왜냐하면 이렇게 하면 부모 Effect 없이 자식 Effect가 두 번 실행되는데, 이는 프로덕션에서 발생할 수 없는 상황이거든요.
부연 설명: 즉, Strict Mode가 일부에만 적용되면 실제 프로덕션 환경과 비슷한 조건에서만 체크가 이루어져요. 전체 앱에 적용했을 때보다 체크가 덜 엄격할 수 있다는 의미죠.
React는 여러분이 작성하는 모든 컴포넌트가 순수 함수라고 가정해요. 이는 React 컴포넌트가 같은 입력(props, state, context)이 주어지면 항상 같은 JSX를 반환해야 한다는 뜻이에요.
이 규칙을 어기는 컴포넌트는 예측할 수 없이 동작하고 버그를 일으켜요. 실수로 작성한 불순한 코드를 찾는 데 도움을 주기 위해, Strict Mode는 개발 환경에서 일부 함수(순수해야 하는 함수만)를 두 번 호출해요. 여기에는 다음이 포함돼요:
useState, set 함수, useMemo, 또는 useReducer에 전달하는 함수들constructor, render, shouldComponentUpdate 등 (전체 목록 보기)함수가 순수하다면, 두 번 실행해도 동작이 변하지 않아요. 순수 함수는 매번 같은 결과를 만들어내니까요. 하지만 함수가 불순하다면(예를 들어, 받은 데이터를 변경한다면), 두 번 실행했을 때 눈에 띄는 차이가 생기는 경향이 있어요 (그게 바로 불순한 이유예요!) 이를 통해 버그를 조기에 발견하고 수정할 수 있어요.
Strict Mode의 이중 렌더링이 버그를 조기에 발견하는 데 어떻게 도움이 되는지 예제를 통해 설명할게요.
이 StoryTray 컴포넌트는 stories 배열을 받아서 마지막에 "Create Story" 항목을 하나 추가해요:
// src/index.js
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
root.render(<App />);
// src/App.js
import { useState } from 'react';
import StoryTray from './StoryTray.js';
let initialStories = [
{id: 0, label: "Ankit's Story" },
{id: 1, label: "Taylor's Story" },
];
export default function App() {
let [stories, setStories] = useState(initialStories)
return (
<div
style={{
width: '100%',
height: '100%',
textAlign: 'center',
}}
>
<StoryTray stories={stories} />
</div>
);
}
// src/StoryTray.js active
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>
);
}
ul {
margin: 0;
list-style-type: none;
height: 100%;
display: flex;
flex-wrap: wrap;
padding: 10px;
}
li {
border: 1px solid #aaa;
border-radius: 6px;
float: left;
margin: 5px;
padding: 5px;
width: 70px;
height: 100px;
}
위 코드에는 실수가 있어요. 하지만 초기 출력이 올바르게 보이기 때문에 놓치기 쉬워요.
StoryTray 컴포넌트가 여러 번 리렌더링되면 이 실수가 더 눈에 띄게 될 거예요. 예를 들어, 마우스를 올릴 때마다 StoryTray가 다른 배경색으로 리렌더링되도록 만들어볼게요:
// src/index.js
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// src/App.js
import { useState } from 'react';
import StoryTray from './StoryTray.js';
let initialStories = [
{id: 0, label: "Ankit's Story" },
{id: 1, label: "Taylor's Story" },
];
export default function App() {
let [stories, setStories] = useState(initialStories)
return (
<div
style={{
width: '100%',
height: '100%',
textAlign: 'center',
}}
>
<StoryTray stories={stories} />
</div>
);
}
// src/StoryTray.js active
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>
);
}
ul {
margin: 0;
list-style-type: none;
height: 100%;
display: flex;
flex-wrap: wrap;
padding: 10px;
}
li {
border: 1px solid #aaa;
border-radius: 6px;
float: left;
margin: 5px;
padding: 5px;
width: 70px;
height: 100px;
}
StoryTray 컴포넌트 위에 마우스를 올릴 때마다 "Create Story"가 목록에 다시 추가되는 걸 주목하세요. 코드의 의도는 마지막에 한 번만 추가하는 거였어요. 하지만 StoryTray는 props에서 받은 stories 배열을 직접 수정하고 있어요. StoryTray가 렌더링될 때마다, 같은 배열의 끝에 "Create Story"를 다시 추가하는 거죠. 다시 말해서, StoryTray는 순수 함수가 아니에요. 여러 번 실행하면 다른 결과가 나오거든요.
이 문제를 해결하려면, 배열의 복사본을 만들고 원본 대신 복사본을 수정하면 돼요:
// {2}
export default function StoryTray({ stories }) {
const items = stories.slice(); // 배열 복제
// ✅ 좋아요: 새 배열에 push하기
items.push({ id: 'create', label: 'Create Story' });
이렇게 하면 StoryTray 함수가 순수해져요. 함수가 호출될 때마다 배열의 새로운 복사본만 수정하고, 외부 객체나 변수에는 영향을 주지 않아요. 이렇게 버그가 해결되지만, 컴포넌트의 동작에 뭔가 문제가 있다는 게 명확해지기 전까지는 컴포넌트를 더 자주 리렌더링해야 했어요.
원래 예제에서는 버그가 명확하지 않았어요. 이제 원래의 (버그가 있는) 코드를 <StrictMode>로 감싸볼게요:
// src/index.js
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
// src/App.js
import { useState } from 'react';
import StoryTray from './StoryTray.js';
let initialStories = [
{id: 0, label: "Ankit's Story" },
{id: 1, label: "Taylor's Story" },
];
export default function App() {
let [stories, setStories] = useState(initialStories)
return (
<div
style={{
width: '100%',
height: '100%',
textAlign: 'center',
}}
>
<StoryTray stories={stories} />
</div>
);
}
// src/StoryTray.js active
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>
);
}
ul {
margin: 0;
list-style-type: none;
height: 100%;
display: flex;
flex-wrap: wrap;
padding: 10px;
}
li {
border: 1px solid #aaa;
border-radius: 6px;
float: left;
margin: 5px;
padding: 5px;
width: 70px;
height: 100px;
}
Strict Mode는 항상 렌더링 함수를 두 번 호출하기 때문에, 실수를 바로 확인할 수 있어요 ("Create Story"가 두 번 나타나요). 이를 통해 이런 실수를 프로세스 초기에 발견할 수 있어요. Strict Mode에서 렌더링되도록 컴포넌트를 수정하면, 이전의 hover 기능 같은 많은 잠재적 프로덕션 버그도 함께 수정하게 돼요:
// src/index.js
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<App />
</StrictMode>
);
// src/App.js
import { useState } from 'react';
import StoryTray from './StoryTray.js';
let initialStories = [
{id: 0, label: "Ankit's Story" },
{id: 1, label: "Taylor's Story" },
];
export default function App() {
let [stories, setStories] = useState(initialStories)
return (
<div
style={{
width: '100%',
height: '100%',
textAlign: 'center',
}}
>
<StoryTray stories={stories} />
</div>
);
}
// src/StoryTray.js active
import { useState } from 'react';
export default function StoryTray({ stories }) {
const [isHover, setIsHover] = useState(false);
const items = stories.slice(); // 배열 복제
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>
);
}
ul {
margin: 0;
list-style-type: none;
height: 100%;
display: flex;
flex-wrap: wrap;
padding: 10px;
}
li {
border: 1px solid #aaa;
border-radius: 6px;
float: left;
margin: 5px;
padding: 5px;
width: 70px;
height: 100px;
}
Strict Mode가 없었다면, 리렌더링을 더 추가하기 전까지는 버그를 놓치기 쉬웠을 거예요. Strict Mode는 같은 버그를 바로 나타나게 만들었어요. Strict Mode는 팀과 사용자에게 버그를 푸시하기 전에 찾을 수 있도록 도와줘요.
컴포넌트를 순수하게 유지하는 방법에 대해 더 알아보세요.
참고
React DevTools가 설치되어 있다면, 두 번째 렌더링 호출 중의
console.log호출은 약간 흐리게 표시돼요. React DevTools는 또한 이를 완전히 숨기는 설정(기본적으로 꺼져 있음)도 제공해요.
Strict Mode는 Effect의 버그를 찾는 데도 도움을 줄 수 있어요.
모든 Effect에는 설정(setup) 코드가 있고, 클린업(cleanup) 코드가 있을 수도 있어요. 일반적으로 React는 컴포넌트가 마운트될 때(화면에 추가될 때) 설정을 호출하고, 컴포넌트가 언마운트될 때(화면에서 제거될 때) 클린업을 호출해요. 그런 다음 마지막 렌더링 이후 의존성이 변경되었다면 React는 클린업과 설정을 다시 호출해요.
Strict Mode가 켜져 있으면, React는 모든 Effect에 대해 개발 환경에서 설정+클린업 사이클을 한 번 더 실행해요. 이게 놀랍게 느껴질 수 있지만, 수동으로 잡기 어려운 미묘한 버그를 드러내는 데 도움이 돼요.
Strict Mode에서 Effect를 재실행하는 것이 버그를 조기에 발견하는 데 어떻게 도움이 되는지 예제를 통해 설명할게요.
채팅에 컴포넌트를 연결하는 이 예제를 생각해보세요:
// src/index.js
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
root.render(<App />);
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>;
}
// src/chat.js
let connections = 0;
export function createConnection(serverUrl, roomId) {
// 실제 구현은 실제로 서버에 연결할 거예요
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);
}
};
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }
이 코드에는 문제가 있지만, 바로 명확하지 않을 수 있어요.
문제를 더 명확하게 만들기 위해, 기능을 구현해볼게요. 아래 예제에서 roomId는 하드코딩되어 있지 않아요. 대신, 사용자가 드롭다운에서 연결하고 싶은 roomId를 선택할 수 있어요. "Open chat"을 클릭한 다음 여러 채팅방을 하나씩 선택해보세요. 콘솔에서 활성 연결 수를 추적해보세요:
// src/index.js
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
root.render(<App />);
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} />}
</>
);
}
// src/chat.js
let connections = 0;
export function createConnection(serverUrl, roomId) {
// 실제 구현은 실제로 서버에 연결할 거예요
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);
}
};
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }
열려 있는 연결 수가 계속 증가하는 걸 볼 수 있어요. 실제 앱에서는 성능 및 네트워크 문제를 일으킬 수 있어요. 문제는 Effect에 클린업 함수가 없다는 거예요:
// {4}
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
이제 Effect가 자신을 "정리"하고 오래된 연결을 파괴하니까 누수가 해결됐어요. 하지만 더 많은 기능(선택 상자)을 추가하기 전까지는 문제가 눈에 띄지 않았다는 점에 주목하세요.
원래 예제에서는 버그가 명확하지 않았어요. 이제 원래의 (버그가 있는) 코드를 <StrictMode>로 감싸볼게요:
// src/index.js
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
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>;
}
// src/chat.js
let connections = 0;
export function createConnection(serverUrl, roomId) {
// 실제 구현은 실제로 서버에 연결할 거예요
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);
}
};
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }
Strict Mode를 사용하면, 문제가 있다는 걸 즉시 알 수 있어요 (활성 연결 수가 2로 점프해요). Strict Mode는 모든 Effect에 대해 추가 설정+클린업 사이클을 실행해요. 이 Effect는 클린업 로직이 없어서 추가 연결을 만들지만 파괴하지는 않아요. 이건 클린업 함수가 빠졌다는 힌트예요.
Strict Mode를 사용하면 이런 실수를 프로세스 초기에 발견할 수 있어요. Strict Mode에서 클린업 함수를 추가해서 Effect를 수정하면, 이전의 선택 상자 같은 많은 잠재적 프로덕션 버그도 함께 수정하게 돼요:
// src/index.js
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
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();
return () => connection.disconnect();
}, [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} />}
</>
);
}
// src/chat.js
let connections = 0;
export function createConnection(serverUrl, roomId) {
// 실제 구현은 실제로 서버에 연결할 거예요
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);
}
};
}
input { display: block; margin-bottom: 20px; }
button { margin-left: 10px; }
콘솔의 활성 연결 카운트가 더 이상 계속 증가하지 않는 걸 주목하세요.
Strict Mode가 없었다면, Effect에 클린업이 필요하다는 걸 놓치기 쉬웠을 거예요. 개발 환경에서 Effect에 대해 설정 대신 설정 → 클린업 → 설정을 실행함으로써, Strict Mode는 누락된 클린업 로직을 더 눈에 띄게 만들었어요.
Strict Mode는 콜백 ref의 버그를 찾는 데도 도움을 줄 수 있어요.
모든 콜백 ref에는 설정(setup) 코드가 있고, 클린업(cleanup) 코드가 있을 수도 있어요. 일반적으로 React는 엘리먼트가 생성될 때(DOM에 추가될 때) 설정을 호출하고, 엘리먼트가 제거될 때(DOM에서 제거될 때) 클린업을 호출해요.
Strict Mode가 켜져 있으면, React는 모든 콜백 ref에 대해 개발 환경에서 설정+클린업 사이클을 한 번 더 실행해요. 이게 놀랍게 느껴질 수 있지만, 수동으로 잡기 어려운 미묘한 버그를 드러내는 데 도움이 돼요.
동물을 선택한 다음 그 중 하나로 스크롤할 수 있는 이 예제를 생각해보세요. "Cats"에서 "Dogs"로 전환할 때, 콘솔 로그를 보면 목록의 동물 수가 계속 증가하고 "Scroll to" 버튼이 작동을 멈추는 걸 알 수 있어요:
// src/index.js
import { createRoot } from 'react-dom/client';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
// ❌ StrictMode를 사용하지 않음.
root.render(<App />);
// src/App.js active
import { useRef, useState } from "react";
export default function CatFriends() {
const itemsRef = useRef([]);
const [catList, setCatList] = useState(setupCatList);
const [cat, setCat] = useState('neo');
function scrollToCat(index) {
const list = itemsRef.current;
const {node} = list[index];
node.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
const cats = catList.filter(c => c.type === cat)
return (
<>
<nav>
<button onClick={() => setCat('neo')}>Neo</button>
<button onClick={() => setCat('millie')}>Millie</button>
</nav>
<hr />
<nav>
<span>Scroll to:</span>{cats.map((cat, index) => (
<button key={cat.src} onClick={() => scrollToCat(index)}>
{index}
</button>
))}
</nav>
<div>
<ul>
{cats.map((cat) => (
<li
key={cat.src}
ref={(node) => {
const list = itemsRef.current;
const item = {cat: cat, node};
list.push(item);
console.log(`✅ Adding cat to the map. Total cats: ${list.length}`);
if (list.length > 10) {
console.log('❌ Too many cats in the list!');
}
return () => {
// 🚩 클린업이 없어요, 이건 버그예요!
}
}}
>
<img src={cat.src} />
</li>
))}
</ul>
</div>
</>
);
}
function setupCatList() {
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({type: 'neo', src: "https://placecats.com/neo/320/240?" + i});
}
for (let i = 0; i < 10; i++) {
catList.push({type: 'millie', src: "https://placecats.com/millie/320/240?" + i});
}
return catList;
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
이건 프로덕션 버그예요! ref 콜백이 클린업에서 동물을 목록에서 제거하지 않기 때문에, 동물 목록이 계속 증가해요. 이건 메모리 누수로, 실제 앱에서 성능 문제를 일으킬 수 있고, 앱의 동작을 망가뜨려요.
문제는 ref 콜백이 자체 정리를 하지 않는다는 거예요:
// {6-8}
<li
ref={node => {
const list = itemsRef.current;
const item = {animal, node};
list.push(item);
return () => {
// 🚩 클린업이 없어요, 이건 버그예요!
}
}}
</li>
이제 원래의 (버그가 있는) 코드를 <StrictMode>로 감싸볼게요:
// src/index.js
import { createRoot } from 'react-dom/client';
import {StrictMode} from 'react';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
// ✅ StrictMode를 사용.
root.render(
<StrictMode>
<App />
</StrictMode>
);
// src/App.js active
import { useRef, useState } from "react";
export default function CatFriends() {
const itemsRef = useRef([]);
const [catList, setCatList] = useState(setupCatList);
const [cat, setCat] = useState('neo');
function scrollToCat(index) {
const list = itemsRef.current;
const {node} = list[index];
node.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
const cats = catList.filter(c => c.type === cat)
return (
<>
<nav>
<button onClick={() => setCat('neo')}>Neo</button>
<button onClick={() => setCat('millie')}>Millie</button>
</nav>
<hr />
<nav>
<span>Scroll to:</span>{cats.map((cat, index) => (
<button key={cat.src} onClick={() => scrollToCat(index)}>
{index}
</button>
))}
</nav>
<div>
<ul>
{cats.map((cat) => (
<li
key={cat.src}
ref={(node) => {
const list = itemsRef.current;
const item = {cat: cat, node};
list.push(item);
console.log(`✅ Adding cat to the map. Total cats: ${list.length}`);
if (list.length > 10) {
console.log('❌ Too many cats in the list!');
}
return () => {
// 🚩 클린업이 없어요, 이건 버그예요!
}
}}
>
<img src={cat.src} />
</li>
))}
</ul>
</div>
</>
);
}
function setupCatList() {
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({type: 'neo', src: "https://placecats.com/neo/320/240?" + i});
}
for (let i = 0; i < 10; i++) {
catList.push({type: 'millie', src: "https://placecats.com/millie/320/240?" + i});
}
return catList;
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
Strict Mode를 사용하면, 문제가 있다는 걸 즉시 알 수 있어요. Strict Mode는 모든 콜백 ref에 대해 추가 설정+클린업 사이클을 실행해요. 이 콜백 ref는 클린업 로직이 없어서 ref를 추가하지만 제거하지는 않아요. 이건 클린업 함수가 빠졌다는 힌트예요.
Strict Mode를 사용하면 콜백 ref의 실수를 빠르게 발견할 수 있어요. Strict Mode에서 클린업 함수를 추가해서 콜백을 수정하면, 이전의 "Scroll to" 버그 같은 많은 잠재적 프로덕션 버그도 함께 수정하게 돼요:
// src/index.js
import { createRoot } from 'react-dom/client';
import {StrictMode} from 'react';
import './styles.css';
import App from './App';
const root = createRoot(document.getElementById("root"));
// ✅ StrictMode를 사용.
root.render(
<StrictMode>
<App />
</StrictMode>
);
// src/App.js active
import { useRef, useState } from "react";
export default function CatFriends() {
const itemsRef = useRef([]);
const [catList, setCatList] = useState(setupCatList);
const [cat, setCat] = useState('neo');
function scrollToCat(index) {
const list = itemsRef.current;
const {node} = list[index];
node.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
const cats = catList.filter(c => c.type === cat)
return (
<>
<nav>
<button onClick={() => setCat('neo')}>Neo</button>
<button onClick={() => setCat('millie')}>Millie</button>
</nav>
<hr />
<nav>
<span>Scroll to:</span>{cats.map((cat, index) => (
<button key={cat.src} onClick={() => scrollToCat(index)}>
{index}
</button>
))}
</nav>
<div>
<ul>
{cats.map((cat) => (
<li
key={cat.src}
ref={(node) => {
const list = itemsRef.current;
const item = {cat: cat, node};
list.push(item);
console.log(`✅ Adding cat to the map. Total cats: ${list.length}`);
if (list.length > 10) {
console.log('❌ Too many cats in the list!');
}
return () => {
list.splice(list.indexOf(item), 1);
console.log(`❌ Removing cat from the map. Total cats: ${itemsRef.current.length}`);
}
}}
>
<img src={cat.src} />
</li>
))}
</ul>
</div>
</>
);
}
function setupCatList() {
const catList = [];
for (let i = 0; i < 10; i++) {
catList.push({type: 'neo', src: "https://placecats.com/neo/320/240?" + i});
}
for (let i = 0; i < 10; i++) {
catList.push({type: 'millie', src: "https://placecats.com/millie/320/240?" + i});
}
return catList;
}
div {
width: 100%;
overflow: hidden;
}
nav {
text-align: center;
}
button {
margin: .25rem;
}
ul,
li {
list-style: none;
white-space: nowrap;
}
li {
display: inline;
padding: 0.5rem;
}
이제 StrictMode에서 초기 마운트 시, ref 콜백이 모두 설정되고, 클린업되고, 다시 설정돼요:
...
✅ Adding animal to the map. Total animals: 10
...
❌ Removing animal from the map. Total animals: 0
...
✅ Adding animal to the map. Total animals: 10
이건 예상된 동작이에요. Strict Mode는 ref 콜백이 올바르게 클린업되는지 확인하므로, 크기가 예상 양을 초과하지 않아요. 수정 후에는 메모리 누수가 없고, 모든 기능이 예상대로 작동해요.
Strict Mode가 없었다면, 앱을 클릭해서 기능이 망가진 걸 발견하기 전까지는 버그를 놓치기 쉬웠을 거예요. Strict Mode는 프로덕션에 푸시하기 전에 버그를 바로 나타나게 만들었어요.
React는 <StrictMode> 트리 내부의 어딘가에 있는 컴포넌트가 다음과 같은 더 이상 사용되지 않는 API 중 하나를 사용하면 경고해요:
UNSAFE_componentWillMount 같은 UNSAFE_ 클래스 생명주기 메서드들. 대안을 확인하세요.이러한 API는 주로 오래된 클래스 컴포넌트에서 사용되기 때문에 최신 앱에는 거의 나타나지 않아요.
부연 설명:
UNSAFE_접두사가 붙은 메서드들은 React의 Concurrent Mode 같은 최신 기능과 호환되지 않아서 사용을 권장하지 않아요. Strict Mode는 이런 오래된 API를 사용하고 있다면 경고를 띄워서, 최신 방식으로 마이그레이션하도록 유도해요.