FE/React

[React] 재귀 컴포넌트로 트리 메뉴 만들기(1)

SH_Roh 2022. 12. 20. 03:24
반응형

재귀 컴포넌트

트리형 메뉴를 만들어야 하는 과제가 있었다. 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

 

반응형