재귀 컴포넌트
트리형 메뉴를 만들어야 하는 과제가 있었다. depth가 깊지 않다면 모든 요소들을 map으로 돌면서 렌더링해주면 되지만 자식 요소들이 많아질 경우 어떻게 구현할 지 고민이 많았다.
이 때 재귀 컴포넌트를 사용하면 된다는 것을 알았다. 재귀 컴포넌트란 말그대로 컴포넌트를 재귀적으로 만드는 것이다.
자세한 내용은 아래에서 직접 구현하면서 알아보자.
데이터 구조
[
{
id: 1,
name: 'Root',
type: 'folder',
children: [
{
id: 2,
name: 'Child 1',
type: 'folder',
children: [
{
id: 3,
name: 'Grand Child',
type: 'file',
},
],
},
{
id: 4,
name: 'Child 2',
type: 'folder',
children: [
{
id: 5,
name: 'Grand Child',
type: 'folder',
children: [
{
id: 6,
name: 'Great Grand Child 1',
type: 'file',
},
{
id: 7,
name: 'Great Grand Child 2',
type: 'file',
},
],
},
],
},
{
id: 8,
name: 'Child 3',
type: 'file',
},
],
}
]
간단하게 데이터를 만들어봤다.
id와 이름, 타입이 있고 타입이 folder일 경우에만 children이 있다.
해당 데이터를 api처럼 호출할 수 있도록 export하는 코드도 추가해주었다.
export interface TreeNode {
id: number
name: string
type: 'folder' | 'file'
children?: TreeNode[]
}
const directoryTree: TreeNode = [{
id: 1,
name: 'Root',
type: 'folder',
children: // ...
}]
export default {
getDirectoryTree: async () => directoryTree,
}
재귀 컴포넌트 구현
useEffect를 통해 컴포넌트가 마운트될 때 아까 만들었던 데이터를 받아와주자.
import api, { TreeNode } from 'services/directory'
import Child from './Child'
const Directory = () => {
const [directory, setDirectory] = useState<TreeNode[]>()
useEffect(() => {
api.getDirectoryTree().then((res) => setDirectory(res))
}, [])
return (
<div>
{directory && (
<ul>
{directory.map((d) => (
<Child key={`${d.id}-${d.name}`} child={d} />
))}
</ul>
)}
</div>
)
}
데이터를 받아온 후에 root 디렉토리의 이름은 li 태그안에 넣어주고, root 디렉토리의 자식요소들은 Child라는 컴포넌트의 props로 넘기도록 한다.
import { TreeNode } from 'services/directory'
interface Props {
child: TreeNode
}
const Child = ({ child }: Props) => {
if (child.type === 'file') {
return <li>{child.name}</li>
}
return (
<>
<li>{child.name}</li>
<li>
<ul>{child.type === 'folder' && child.children?.map((c) => <Child key={`${c.id}`} child={c} />)}</ul>
</li>
</>
)
}
Child라는 이름의 재귀 컴포넌트이다.
props로 받은 data를 map으로 돌면서 각 child의 이름을 렌더링하고, 만약 해당 child가 폴더라면 그 안에서 다시 Child 컴포넌트를 렌더링할 수 있도록 child의 children을 props으로 넘겨준다.
이렇게 하면 순서에 맞게 모든 데이터들이 렌더링된다. 하지만 현재는 폴더의 계층 구조를 알아보기 힘들다. depth를 추가해서 계층 구조를 나타내보자.
전체 코드
services/directory.ts
export interface TreeNode {
id: number
name: string
type: 'folder' | 'file'
children?: TreeNode[]
}
const directoryTree: TreeNode[] = [
{
id: 1,
name: 'Root',
type: 'folder',
children: [
{
id: 2,
name: 'Child 1',
type: 'folder',
children: [
{
id: 3,
name: 'Grand Child',
type: 'file',
},
],
},
{
id: 4,
name: 'Child 2',
type: 'folder',
children: [
{
id: 5,
name: 'Grand Child',
type: 'folder',
children: [
{
id: 6,
name: 'Great Grand Child 1',
type: 'file',
},
{
id: 7,
name: 'Great Grand Child 2',
type: 'file',
},
],
},
],
},
{
id: 8,
name: 'Child 3',
type: 'file',
},
],
},
]
export default {
getDirectoryTree: async () => directoryTree,
}
routes/Directory/index.tsx
import { useEffect, useState } from 'react'
import api, { TreeNode } from 'services/directory'
import Child from './Child'
const Directory = () => {
const [directory, setDirectory] = useState<TreeNode[]>()
useEffect(() => {
api.getDirectoryTree().then((res) => setDirectory(res))
}, [])
return (
<div>
{directory && (
<ul>
{directory.map((d) => (
<Child key={`${d.id}-${d.name}`} child={d} />
))}
</ul>
)}
</div>
)
}
export default Directory
routes/Directory/Child.tsx
import { TreeNode } from 'services/directory'
interface Props {
child: TreeNode
}
const Child = ({ child }: Props) => {
if (child.type === 'file') {
return <li>{child.name}</li>
}
return (
<>
<li>{child.name}</li>
<li>
<ul>{child.type === 'folder' && child.children?.map((c) => <Child key={`${c.id}`} child={c} />)}</ul>
</li>
</>
)
}
export default Child
References
https://naveenda.medium.com/how-to-recursively-render-the-react-component-a821b3532894
https://garve32.tistory.com/m/52
'FE > React' 카테고리의 다른 글
react-datepicker 적용하기(+ 커스텀) (0) | 2023.01.10 |
---|---|
[React] 재귀 컴포넌트로 트리 메뉴 만들기(2) (0) | 2022.12.26 |
[React] Storybook 사용해보기 (0) | 2022.12.02 |
React Infinite Carousel 만들기(TS) (1) | 2022.11.24 |
React 스톱워치 구현하기(TS) (0) | 2022.11.17 |