개요
유지보수를 하면서 이전에 작성된 코드를 보는데 `{...props}` 문법이 정말 많이 보였다. 리액트의 문법들에 대해 다 알지 못했기에 무슨 이런 해괴한 문법이 있나... 하면서 찾아보니 꽤나 유용한 문법이었다. 처음 봤을 때는 솔직히 조금 불편했다. 명시적으로 어떤게 있는 지 모르니 말이다... 그러면서 이건 아무래도 안티 패턴인거 같다는 생각이 들었고, 커뮤니티랑 글들을 찾아보니 내 생각이 맞았다.
얼핏 보면 코드도 간결하고, 고수처럼 코딩하는 것 처럼 보인다.
// 이렇게 간단하게 props를 넘길 수 있다니!
function ParentComponent() {
const userProps = { name: '노근', age: 25, job: 'Developer' };
return <UserCard {...userProps} />;
}
하지만 나의 첫인상대로, 성능 문제가 발생하고, 코드 리뷰에서 "이게 뭐가 넘어가는 거예요?"라는 질문을 받고, 리팩토링할 때 "이 prop 지워도 되나?" 확신이 안 서는 상황들이 반복됐다. 지금부터 왜 `{...props}`가 안티 패턴인지, 그리고 언제 조심해야 하는지 정리해보려 한다.
문제점들
1. 가독성 저하
무엇이 전달되는지 알 수 없다
코드를 읽을 때 가장 답답한 순간은 "이 컴포넌트가 뭘 받는 거지?"가 한눈에 보이지 않을 때다. ▼
// ❌ 이 코드만 봐서는 UserCard가 뭘 받는지 알 수 없다
function UserList({ users, currentTime, isLoading, filters, ...other }) {
return users.map(user => (
<UserCard key={user.id} {...user} {...other} />
));
}
`UserCard`가 어떤 `props`를 받는지 알려면 `user` 객체가 뭘 포함하는지, `other`에 뭐가 들어있는지 일일이 추적해야 한다. 부모 컴포넌트로 올라가서 확인하고, 타입 정의 파일도 뒤져봐야 한다.
실제로 겪은 상황
리팩토링을 하다가 이런 식의 코드를 발견했다. (실제 회사 코드는 아닙니다. 예시를 위한 코드일 뿐입니다.) ▼
function ProductCard({ product, ...rest }: ProductCardProps) {
return (
<Card {...rest}>
<div>{product.name}</div>
<div>{product.price}</div>
</Card>
);
}
처음 보자마자 든 의문은 'rest에 뭐가 들어있지?'였다. 그래서 이를 확인하기 위해 부모 컴포넌트를 확인했다. ▼
<ProductCard
product={data}
className="custom-card"
onClick={handleClick}
currentUser={user}
debugMode={isDev}
lastUpdated={timestamp}
/>
`Card` 컴포넌트는 `className`과 `onClick`만 필요한데, `currentUser`, `debugMode`, `lastUpdated` 같은 불필요한 `props`까지 전부 넘어가고 있었다. Depth가 깊지 않으면 금방 찾지만, 나의 경우 4depth까지 내려갔기에 생각보다 이해하는데에 시간이 꽤 걸렸다.
2. 불필요한 Props 전달과 DOM 오염
DOM에 유효하지 않은 속성이 렌더링된다
HTML 태그에 직접 spread를 사용하면 React 전용 props나 커스텀 props가 DOM에 그대로 렌더링될 수 있다. ▼
// ❌ 이렇게 하면 안 된다
function Button({ label, ...props }) {
return <button {...props}>{label}</button>;
}
// 사용
<Button
label="클릭"
onClick={handleClick}
isActive={true} // ← 이게 DOM에 들어가면 안 되는데...
customData="abc"
/>
브라우저 콘솔을 열어보면 이런 경고가 뜬다. ▼
Warning: React does not recognize the `isActive` prop on a DOM element.
Warning: React does not recognize the `customData` prop on a DOM element.
결과적으로 HTML은 이렇게 렌더링된다. ▼
<button isActive="true" customData="abc">클릭</button>
HTML 표준에 없는 속성들이 DOM에 그대로 박혀버린다. 이건 단순히 경고만 뜨는 게 아니라, HTML 유효성 검사에서도 걸리고, 접근성 도구들도 헷갈려한다.
해결 방법
필요한 것만 명시적으로 전달하거나, DOM으로 넘어갈 props만 걸러내야 한다. ▼
// ✅ 좋은 예 1: 필요한 것만 전달
function Button({ label, onClick, disabled, className }) {
return (
<button onClick={onClick} disabled={disabled} className={className}>
{label}
</button>
);
}
// ✅ 좋은 예 2: DOM 안전한 props만 걸러내기
function Button({ label, isActive, customData, ...domProps }) {
// isActive, customData는 로직에만 사용하고 DOM에는 안 넘김
return (
<button {...domProps} className={isActive ? 'active' : ''}>
{label}
</button>
);
}
물론 이 정도는 사람이 충분히 걸러낼 수 있다. HTML 태그인지, React 태그인지는 보는 것 만으로도 쉽게 구분이 되니 말이다. 다만 한 번 발생하면 어디서 발생하는건지 찾기가 까다롭기 때문에 조심해야한다.
3. 불필요한 리렌더링
이게 가장 치명적인 문제다. 성능 이슈가 직접적으로 발생한다.
시나리오: 1초마다 업데이트되는 타이머
부모 컴포넌트에 1초마다 업데이트되는 타이머가 있다고 해보자. ▼
function Dashboard() {
const [currentTime, setCurrentTime] = useState(new Date());
const [userName, setUserName] = useState('노근');
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
// 모든 props를 spread로 넘김
return (
<UserProfile
{...{ userName, currentTime }}
/>
);
}
function UserProfile(props) {
console.log('UserProfile 렌더링!');
return <div>{props.userName}</div>;
}
`UserProfile`은 `userName`만 표시하는데, `currentTime`도 함께 받고 있다. 결과적으로 `userName`은 전혀 변하지 않았음에도 `UserProfile`은 1초마다 리렌더링된다.
콘솔을 보면
UserProfile 렌더링! // 매 초 마다 나온다
UserProfile 렌더링!
UserProfile 렌더링!
...
React.memo도 무력화된다
성능 최적화를 위해 `React.memo`를 적용해도 소용없다. ▼
// React.memo로 감쌌지만...
const UserProfile = React.memo((props) => {
console.log('UserProfile 렌더링!');
return <div>{props.userName}</div>;
});
function Dashboard() {
const [currentTime, setCurrentTime] = useState(new Date());
const extraProps = { style: { color: 'red' }, id: 'profile' };
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
// ❌ 매 렌더링마다 새로운 객체를 만들어서 spread
return <UserProfile userName="노근" {...extraProps} />;
}
`extraProps`는 매 렌더링마다 새로운 객체로 생성된다. 내용은 똑같아도 참조가 다르기 때문에 React는 props가 바뀌었다고 판단하고 `React.memo`를 무시하고 리렌더링한다.
올바른 해결책
필요한 것만 명시적으로 전달한다. ▼
const UserProfile = React.memo(({ userName }) => {
console.log('UserProfile 렌더링!');
return <div>{userName}</div>;
});
function Dashboard() {
const [currentTime, setCurrentTime] = useState(new Date());
const [userName, setUserName] = useState('노근');
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
// userName만 전달 - currentTime이 바뀌어도 UserProfile은 리렌더링 안 됨
return <UserProfile userName={userName} />;
}
이제 `UserProfile`은 `userName`이 실제로 바뀔 때만 렌더링된다.
문제점 4: 리팩토링의 어려움
이 `prop` 지워도 되나?
프로젝트를 정리하다가 더 이상 사용하지 않는 `prop`을 발견했다고 해보자. ▼
interface UserCardProps {
name: string;
age: number;
email: string;
legacyId?: string; // ← 이거 아무데서도 안 쓰는 것 같은데...
}
function UserCard(props: UserCardProps) {
return (
<div>
<h3>{props.name}</h3>
<p>{props.email}</p>
</div>
);
}
`legacyId`를 지워도 되나? 확신이 안 선다. 왜냐하면 어디선가 이렇게 쓰고 있을 수 있기 때문이다. ▼
function UserList({ users }) {
return users.map(user => (
// {...user}로 넘기면 legacyId도 자동으로 넘어감
<UserCard key={user.id} {...user} />
));
}
`UserCard` 내부에서는 안 쓰지만, spread로 넘기는 어딘가에서 필요할 수도 있다. 전체 프로젝트를 뒤져봐야 안전하게 지울 수 있다.
명시적으로 전달하면?
function UserList({ users }) {
return users.map(user => (
<UserCard
key={user.id}
name={user.name}
age={user.age}
email={user.email}
/>
));
}
이렇게 하면 `legacyId`는 애초에 전달되지 않는다. IDE에서 "Unused prop"이라고 표시해주고, 안심하고 지울 수 있다.
그럼 언제 Spread를 써도 되나?
물론 모든 spread가 나쁜 건 아니다. 적절한 상황도 있다.
1. HOC (Higher Order Component)
컴포넌트를 감싸서 기능을 추가하는 HOC에서는 원래 `props`를 그대로 전달해야 한다. ▼
// ✅ HOC에서는 spread 사용이 적절하다
function withAuth<P extends object>(Component: React.ComponentType<P>) {
return function AuthenticatedComponent(props: P) {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Redirect to="/login" />;
}
// 원래 컴포넌트가 받던 모든 props를 그대로 전달
return <Component {...props} />;
};
}
2. 범용 UI 라이브러리
내부 구현을 숨기고 유연하게 사용할 수 있게 하는 컨테이너 컴포넌트에서도 유용하다. ▼
// ✅ 범용 Box 컴포넌트
function Box({ children, ...htmlProps }: BoxProps) {
// 사용자가 주입한 모든 HTML 속성을 그대로 전달
return <div {...htmlProps}>{children}</div>;
}
// 사용
<Box className="mt-4" onClick={handleClick} data-testid="container">
내용
</Box>
다만 이 경우에도 타입을 명확히 해야 한다. ▼
interface BoxProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
실제 개선 사례
Spread 사용 코드
function ArticleList({ articles, user, currentTime, filters, sort, ...rest }) {
return (
<div {...rest}>
{articles.map(article => (
<ArticleCard
key={article.id}
{...article}
{...rest}
user={user}
/>
))}
</div>
);
}
function ArticleCard(props) {
// props에 뭐가 들어있는지 알 수 없음
// 리렌더링도 자주 발생
return (
<div>
<h3>{props.title}</h3>
<p>{props.content}</p>
</div>
);
}
명시적 전달 코드
interface ArticleListProps {
articles: Article[];
className?: string;
onArticleClick?: (articleId: string) => void;
}
function ArticleList({ articles, className, onArticleClick }: ArticleListProps) {
return (
<div className={className}>
{articles.map(article => (
<ArticleCard
key={article.id}
title={article.title}
content={article.content}
author={article.author}
onClick={() => onArticleClick?.(article.id)}
/>
))}
</div>
);
}
interface ArticleCardProps {
title: string;
content: string;
author: string;
onClick?: () => void;
}
const ArticleCard = React.memo(({
title,
content,
author,
onClick
}: ArticleCardProps) => {
return (
<div onClick={onClick}>
<h3>{title}</h3>
<p>{content}</p>
<span>{author}</span>
</div>
);
});
명시적 전달로 바꾸면 코드가 조금 더 길어지긴 하지만 다음 요소들이 개선된다.
- 코드 가독성 향상 (각 컴포넌트가 받는 props가 명확)
- 불필요한 리렌더링 감소 (필요한 것만 전달)
- 리팩토링 용이 (사용하지 않는 prop 식별 가능)
- 타입 안정성 향상 (TypeScript가 정확히 타입 체크)
마무리
`{...props}`는 편리하다. 코드도 짧아지고, 타이핑도 적게 하고, 뭔가 간결한 코드를 쓰는 것 같은 만족감도 준다. 하지만 그 편리함 뒤에는 가독성 저하, 성능 이슈, 유지보수의 어려움이 숨어있다.
명시적인 것이 암시적인 것보다 낫다
..는 Python의 철학은 React에도 그대로 적용된다. 코드 몇 줄 줄이려고 장기적인 유지보수성을 희생하는 건 좋은 거래가 아니다.
물론 HOC나 범용 컴포넌트 같은 예외 상황도 있다. 하지만 일반적인 비즈니스 로직 컴포넌트에서는 가능하면 필요한 prop을 하나하나 명시해서 전달하는 게 맞다. 처음엔 귀찮아도, 나중에 그 코드를 다시 볼 때 (혹은 팀원이 볼 때) 훨씬 이해하기 쉽고 고치기 쉬운 코드가 된다.
조금 번거롭더라도 명확한 코드가 결국 더 좋은 코드다.
'Develop > Web' 카테고리의 다른 글
| [Issue] Firefox에서 새로고침 시 Input 값이 초기화되지 않는 문제 해결하기 (1) | 2025.11.20 |
|---|---|
| [Web] div 수프를 끓이지 말아야 하는 이유 (0) | 2025.11.02 |
| [React][Error] tailwind css 설치 오류 (4) | 2025.10.05 |
| [NextJs] 하이아크 홈페이지 모노레포 적용기 (0) | 2025.09.23 |
| [React] 웹 프로젝트에서의 클린아키텍쳐 (0) | 2025.09.10 |