import React from "react";
export default function App() {
return (
<>
<h4>데이터 테이블</h4>
<table>
<thead>
<tr>
<th>name</th>
<th>job</th>
<th>age</th>
</tr>
</thead>
<tbody>
<tr>
<td>mo<td>
<td>dev<td>
<td>22<td>
</tr>
<tr>
<td>lee<td>
<td>dentist<td>
<td>22<td>
</tr>
</tbody>
</table>
</>
);
}
정적인 데이터를 활용해 단순히 렌더링만 해주기 때문에
테이블의 모양이 바뀌거나 데이터가 다른 경우 재사용성이 낮아진다
이 쯤에서 보통 "테이블 렌더링을 데이터를 받는 컴포넌트로 분리해보자" 라고 생각하게 된다.
Table.tsx
interface TableProps {
columns: Array<"name" | "job" | "age">;
data: { name: string; job: string; age: number; }[];
}
export default function Table({ columns, data }: TableProps) {
return (
<table>
<thead>
<tr>
{columns.map((column, i) => (<th key={i}>{column}</th>)}
</tr>
</thead>
<tbody>
{data.map((row, i) => {
return (
<tr key={i}>
{column.map((column, j) => {
return (<td key={j}>{row[column]}</td>)
})}
</tr>
)}
)}
</tbody>
</table>
);
}
App.tsx
import Table from "./Table";
const data = [
{ name: "mo", job: "dev", age 22 },
{ name: "lee", job: "dentist", age: 22 }
];
export default function App() {
return (
<>
<h4>데이터 테이블</h4>
<Table column={["name", "job", "age"]} data={data} />
</>
)
}
data를 전달해 그려주는 컴포넌트로 만들었다!
column도 선택적으로 전달해서 괜찮은 컴포넌트 처럼 보인다.
하지만 넘겨주는 데이터의 타입이 { name: string; job: string; age: number }
로 고정되어 있기 때문에 타입이 달라지면 이 Table 컴포넌트를 사용할 수 없다.
컴포넌트를 재사용하기 위해서는 타입을 지정하지 않고 "주입" 받아야 한다.
다른 언어에도 종종 보이는 제네릭 타입을 사용하면 해결할 수 있다.
interface TableProps<T> {
columns: Array<keyof T>;
data: T[];
}
제네릭 타입으로 테이블 컴포넌트의 prop을 수정하여 data의 모양을 type prop T로 적용한다.
column는 해당 data의 키만 사용가능해야 하므로 Array<keyof T>
를 사용한다.
이렇게 제한해야 <Table />
컴포넌트를 사용할 때 column 속성으로 넘겨주는 키값을 타입스크립트가 추론해서 자동완성을 사용할 수 있고
사용자가 데이터에 존재하지 않는 컬럼명을 입력하는 타입에러를 사전에 방지할 수 있다.
지금까지 테이블 컴포넌트는 데이터를 받아서 렌더링해주고, column 필드의 타입을 추론하여 사용자의 실수를 방지할 수 있다.
하지만 테이블을 단순히 텍스트를 그려주기만 할 뿐이라 각 컬럼의 요소를 선택적으로 다르게 렌더링한다거나 클릭 등 사용자 액션은 할 수 없다.
여기서 테이블 컴포넌트의 자식 요소를 사용자에게 넘기고 컴포넌트를 사용하는 쪽에서 제어할 수 있도록하면 더 자유로운 컴포넌트가 된다.
그러면 어떻게 해야할까?
컴포넌트의 property를 사용하는 게 아닌 children
을 사용하면 된다.
컴포넌트에서는 사용자에로부터 받은 children comp를 실행하고, 사용자는 children comp를 테이블 컴포넌트 하위로 작성하여 자유롭게 반환 컴포넌트를 만들 수 있다.
interface TableProps<T> {
columns: Array<keyof T>;
data: T[];
children: () => JSX.Element;
}
// Table Comp의 tbody
<tbody>
{data.map((row, i) => {
return (
<tr key={i}>
{column.map((column, j) => {
/* 전달받은 children으로 수정된 코드 */
return (<td key={j}>
{children()}
</td>)
})}
</tr>
)}
)}
</tbody>
interface TableProps<T> {
columns: Array<keyof T>;
data: T[];
children: (payload: { rowData: T, property: keyof T, index?: number }) => JSX.Element;
}
// Table Comp의 tbody
<tbody>
{data.map((row, i) => {
return (
<tr key={i}>
{column.map((column, j) => {
/* 전달받은 children으로 수정된 코드 */
return (<td key={j}>
{children({ rowData: row, property: column, index: i})}
</td>)
})}
</tr>
)}
)}
</tbody>
이렇게하면 테이블 컴포넌트를 실행될 때 children을 rowData, property, index를 컴포넌트를 사용하는 사용자에게 넘겨준다.
App.tsx
export default function App() {
return (
<>
<h4>데이터 테이블</h4>
<Table column={["name", "job", "age"]} data={data}>
{({ rowData, property, index }) => {
if (property === "job") {
return (<span>{rowData[property} 👍</span>);
}
return (
<React.Fragment key={index}>
{rowData[property}
</React.Fragment>
);
}}
</Table>
</>
)
}
Table 컴포넌트를 사용할 때 children 함수에서 반환하는 값에 따라 다르게 렌더링할 수 있게 됐다!! 🙌
컴포넌트를 만들 때 사용자에게 일방향으로 전달받아 렌더링하는 컴포넌트를 생각하는 게 일반적인데,
이런식으로 활용하면 컴포넌트를 사용하는 사용자에게 제어를 넘겨줄 수 있어서 더 사용성 높은 컴포넌트를 만들 수 있다.
import React from "react";
import "./styles.css";
const data = [
{ name: "mo", job: "dev", age 22 },
{ name: "lee", job: "dentist", age: 22 }
];
const data2 = [
{ brand: "samsung", industry: "반도체", employees: 1_000_222 },
{ brand: "apple", industry: "농장", employees: 50_000_222 }
];
interface TableProps<T> {
columns: Array<keyof T>;
data: T[];
children: (payload: { rowData: T; property: keyof T; index?: number; }) => JSX.Element;
}
function Table<T>({ columns, data, children }: TableProps<T>) {
return (
<table>
<thead>
<tr>
{columns.map((column, i) => (
<th key={i}>{column as React.ReactNode}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map((column, j) => (
<td key={j}>
{children({ rowData: row, property: column, index: j })}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
export default function App() {
return (
<div className="App">
<h4>데이터 테이블</h4>
<Table columns={["name", "job", "age"]} data={data}>
{({ rowData, property, index }) => {
if (property === "job") {
return (<span>{rowData[property]} 👍</span>)
}
return (
<React.Fragment key={index}>{rowData[property]}</React.Fragment>
);
}}
</Table>
</div>
);
}