💡 에러 개요
React + TypeScript로 작업 중 App.tsx에서 MainLayout 컴포넌트를 사용하는 부분에서 아래와 같은 타입 오류가 발생했다.
// App.tsx
import { Outlet } from "react-router-dom";
import MainLayout from "./layouts/MainLayout";
function App() {
return (
<MainLayout>
<Outlet />
</MainLayout>
);
}
⚠️ 발생한 에러 메시지
Type '{ children: Element; }' has no properties in common with type 'IntrinsicAttributes'.ts(2559)
(alias) function MainLayout(): JSX.Element
🔍 에러 원인
이 에러는 MainLayout 컴포넌트가 children을 props로 받을 수 있도록 정의되어 있지 않기 때문에 발생한 것이다.
TypeScript에서는 props로 어떤 값이 들어오는지를 명시적으로 타입 선언해줘야 하며,
그렇지 않으면 해당 JSX 태그에 전달되는 속성들이 허용되지 않는다고 판단하고 에러를 발생시킨다.
📚 관련 개념 정리
- children : React 컴포넌트 내부에서 다른 컴포넌트를 감싸 사용할 수 있게 해주는 특별한 prop이다.
- ReactNode : React에서 렌더링 가능한 모든 요소를 포함하는 타입이다.JSX 요소, 문자열, 숫자, 배열, fragment, null 등 모두 포함된다.
- IntrinsicAttributes : JSX 문법을 해석할 때 사용되는 React 내부 타입이다. props가 명시되지 않은 컴포넌트에 값을 전달하려고 하면 타입이 일치하지 않아 에러가 발생할 수 있다.
✅ 해결 방법
MainLayout 컴포넌트에 전달되는 children의 타입을 명시해주면 해결된다. 아래와 같이 수정한다.
// src/layouts/MainLayout.tsx
import { ReactNode } from "react";
type MainLayoutProps = {
children: ReactNode;
};
const MainLayout = ({ children }: MainLayoutProps) => {
return (
<div className="max-w-screen-md mx-auto px-4 py-6">
{children}
</div>
);
};
export default MainLayout;
이제 App.tsx에서는 아래와 같이 문제없이 사용할 수 있다.
<MainLayout>
<Outlet />
</MainLayout>
⚠️ 연장선상에서 발생한 두 번째 오류 :
Property 'children' is missing in type '{}' but required in type 'MainLayoutProps'.ts(2741)
MainLayout.tsx(7, 3): 'children' is declared here.
(alias) function MainLayout({ children }: MainLayoutProps): JSX.Element
import MainLayout
이는 MainLayout 컴포넌트가 여전히 children을 필수 props로 받고 있기 때문에, createBrowserRouter에서 다음처럼 사용했을 때:
element: <MainLayout />,
React Router가 children을 직접 전달하지 않아서 타입 오류가 발생한 것이다.
✅ 해결 방법: MainLayout을 React Router 전용으로 리팩토링
이 경우, MainLayout 컴포넌트는 children 대신 Outlet을 사용해야 한다.
React Router의 <Outlet />은 중첩 라우팅 구조에서 하위 라우트의 컴포넌트를 렌더링해주는 전용 컴포넌트다.
아래와 같이 MainLayout을 수정한다:
// src/layouts/MainLayout.tsx
import { Outlet } from 'react-router-dom';
import Header from '../components/layout/Header';
import Footer from '../components/layout/Footer';
export default function MainLayout() {
return (
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-1 px-4 py-8 max-w-screen-xl mx-auto w-full">
<Outlet />
</main>
<Footer />
</div>
);
}
이제 router.tsx에서도 문제없이 사용 가능하다:
{
path: '/',
element: <MainLayout />,
children: [
{ index: true, element: <Home /> },
{ path: 'list', element: <List /> },
...
],
}
📝 정리
React + TypeScript 환경에서는 컴포넌트에 전달되는 props의 타입을 명확하게 정의하는 것이 매우 중요하다.
특히 `children`을 사용하는 컴포넌트는 반드시 `props` 타입에 `children: ReactNode`를 포함해야 하며,
이를 누락하면 JSX 요소를 감싸는 구조에서 타입 오류가 발생할 수 있다.
또한, React Router의 `createBrowserRouter`를 사용할 때 `element`로 전달되는 컴포넌트는
`children`을 직접 받는 구조가 아니라 `<Outlet />`을 통해 중첩 라우트를 렌더링해야 한다.
따라서 레이아웃 컴포넌트라면 상황에 따라 `props.children`이 아닌 `Outlet`을 활용한 구성으로 작성해야 타입 오류 없이 동작한다.
결국 이슈의 핵심은 "컴포넌트의 역할에 맞는 props 설계와 타입 정의"이며,
레이아웃처럼 JSX 요소를 감싸거나 라우팅 구조를 담당하는 컴포넌트는 그 용도에 따라
`children` 또는 `Outlet` 중 적절한 방식을 선택해야 한다.
🔚 마무리
이슈의 핵심은 컴포넌트에서 children의 타입을 명시하지 않아 발생한 오류와, React Router의 `element` 속성에서
children을 전달하지 않는데도 props로 요구해 생기는 타입 오류라는 두 가지 문제다.
첫 번째는 `children`을 props로 받는 컴포넌트에서 타입을 명확히 정의하지 않아 생긴 문제이며,
두 번째는 React Router에서는 children이 자동으로 전달되지 않기 때문에 `<Outlet />`을 사용하는 구조로 변경해야 해결되는 문제다.
두 이슈 모두 컴포넌트의 용도에 따라 적절한 타입 정의와 구조 설계만 해주면 쉽게 해결할 수 있다.
하지만 TypeScript를 처음 접하는 입장에서는 에러 메시지가 익숙하지 않고 원인 파악도 쉽지 않기 때문에,
비슷한 문제를 겪는 개발자들에게 이 글이 도움이 되었으면 한다.
앞으로도 레이아웃 컴포넌트나 중첩 라우팅을 설계할 때는 props와 타입의 흐름을 명확히 이해하고, 상황에 맞는 구조(`children` vs `Outlet`)를 선택하는 습관을 들이는 것이 중요하다.
댓글