
Props로 받은 객체가 undefined일 때
부모에서 전달한 props가 undefined로 나와서 앱이 크래시되는 문제 해결

부모에서 전달한 props가 undefined로 나와서 앱이 크래시되는 문제 해결
분명히 클래스를 적었는데 화면은 그대로다? 개발자 도구엔 클래스가 있는데 스타일이 없다? Tailwind 실종 사건 수사 일지.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

서버에서 잘 오던 데이터가 갑자기 앱을 죽입니다. 'type Null is not a subtype of type String' 에러의 원인과, 안전한 JSON 파싱을 위한 Null Safety 전략을 정리해봤습니다.

습관적으로 모든 변수에 `useMemo`를 감싸고 있나요? 그게 오히려 성능을 망치고 있습니다. 메모이제이션 비용과 올바른 최적화 타이밍.

제 서비스에서 사용자 프로필 페이지를 만들고 있었습니다. 부모 컴포넌트에서 API로 사용자 데이터를 불러온 다음, 자식 컴포넌트에 전달하는 구조였죠.
function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(data => setUser(data));
}, []);
return <UserCard user={user} />;
}
function UserCard({ user }) {
return (
<div>
<h1>{user.name}</h1> {/* 🔥 Cannot read property 'name' of null */}
<p>{user.email}</p>
</div>
);
}
이 코드를 실행하면 앱이 크래시됩니다. 콘솔에 빨간 에러가 뜨면서 "Cannot read property 'name' of null"이라고 나오죠.
처음엔 "분명히 API에서 데이터를 받아오는데 왜 null이지?"라고 생각했습니다. 하지만 문제는 타이밍이었습니다. API 응답이 오기 전에 컴포넌트가 먼저 렌더링되는 거였어요.
제가 가진 오개념은 이거였습니다: "useEffect에서 데이터를 불러오면 컴포넌트가 렌더링될 때 이미 데이터가 있다"
하지만 React의 렌더링 순서는 이렇습니다:
user는 null)useEffect 실행setUser 호출user에 데이터 있음)문제는 1번 단계에서 이미 user.name을 읽으려고 한다는 겁니다. 이때는 아직 user가 null이니까 에러가 나는 거죠.
"그럼 어떻게 해야 하지?"라는 생각이 들었습니다. 데이터가 없을 때는 어떻게 처리해야 할까요?
이 문제를 이해한 건 이런 비유를 들었을 때였습니다:
"컴포넌트는 레스토랑 주방이고, props는 재료다. 재료가 아직 안 왔는데 요리를 시작하면 당연히 문제가 생긴다. 재료가 올 때까지 기다리거나, 대체 재료를 써야 한다."아! 그래서 로딩 상태나 옵셔널 체이닝을 써야 하는 거구나.
해결책은 여러 가지입니다:
해결책 1: 로딩 상태 추가function ProfilePage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser()
.then(data => setUser(data))
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <UserCard user={user} />;
}
해결책 2: 옵셔널 체이닝
function UserCard({ user }) {
return (
<div>
<h1>{user?.name ?? 'Unknown'}</h1>
<p>{user?.email ?? 'No email'}</p>
</div>
);
}
해결책 3: 기본값 설정
function UserCard({ user = {} }) {
const { name = 'Unknown', email = 'No email' } = user;
return (
<div>
<h1>{name}</h1>
<p>{email}</p>
</div>
);
}
저는 보통 해결책 1을 선호합니다. 사용자에게 로딩 중임을 명확히 보여주니까요.
React 컴포넌트의 렌더링 순서를 정확히 이해하는 게 중요합니다:
function Component() {
console.log('1. Render phase');
useEffect(() => {
console.log('3. Effect phase (after render)');
});
return <div>2. JSX returned</div>;
}
실행 순서:
useEffect는 렌더링 후에 실행됩니다. 그래서 첫 렌더링 때는 아직 데이터가 없는 거죠.
TypeScript를 쓰면 이런 문제를 컴파일 타임에 잡을 수 있습니다:
interface User {
name: string;
email: string;
}
interface UserCardProps {
user: User | null; // null 가능성 명시
}
function UserCard({ user }: UserCardProps) {
if (!user) {
return <div>Loading...</div>;
}
// 이제 user는 확실히 User 타입
return (
<div>
<h1>{user.name}</h1> {/* ✅ 타입 안전 */}
<p>{user.email}</p>
</div>
);
}
TypeScript가 "user가 null일 수 있으니 체크하세요"라고 경고해줍니다.
실제 API 응답은 보통 중첩된 객체입니다:
const user = {
profile: {
personal: {
name: 'John',
age: 30
},
contact: {
email: 'john@example.com'
}
}
};
이럴 때 옵셔널 체이닝이 빛을 발합니다:
// 🔥 위험: 중간에 하나라도 undefined면 크래시
const name = user.profile.personal.name;
// ✅ 안전: 중간에 undefined 있으면 undefined 반환
const name = user?.profile?.personal?.name;
배열도 마찬가지입니다:
function UserList({ users }) {
return (
<ul>
{users.map(user => ( // 🔥 users가 undefined면 크래시
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// ✅ 안전한 버전
function UserList({ users = [] }) {
if (users.length === 0) {
return <div>No users</div>;
}
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
React 18부터는 Suspense를 써서 더 우아하게 처리할 수 있습니다:
function ProfilePage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<ErrorBoundary fallback={<div>Error!</div>}>
<UserCard />
</ErrorBoundary>
</Suspense>
);
}
function UserCard() {
const user = use(fetchUser()); // React 19의 use hook
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
이렇게 하면 로딩과 에러 처리를 선언적으로 할 수 있습니다.
제 프로필 페이지를 이렇게 개선했습니다:
function ProfilePage() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser()
.then(data => setUser(data))
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return <ErrorMessage message={error} />;
}
if (!user) {
return <div>User not found</div>;
}
return <UserCard user={user} />;
}
이제 모든 경우를 처리합니다: 로딩 중, 에러, 데이터 없음, 정상.
이 패턴을 자주 쓰니까 커스텀 훅으로 만들었습니다:
function useFetch(fetchFn) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchFn()
.then(setData)
.catch(err => setError(err.message))
.finally(() => setLoading(false));
}, []);
return { data, loading, error };
}
// 사용
function ProfilePage() {
const { data: user, loading, error } = useFetch(fetchUser);
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
if (!user) return <div>User not found</div>;
return <UserCard user={user} />;
}
훨씬 깔끔하죠!
폼에서도 비슷한 문제가 있었습니다:
function EditProfile({ initialData }) {
const [formData, setFormData] = useState(initialData);
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
return (
<form>
<input
name="name"
value={formData.name} // 🔥 initialData가 undefined면 크래시
onChange={handleChange}
/>
</form>
);
}
기본값을 설정해서 해결했습니다:
function EditProfile({ initialData = {} }) {
const [formData, setFormData] = useState({
name: '',
email: '',
...initialData // 있으면 덮어쓰기
});
// 이제 안전!
}