본문 바로가기
리액트

[react] 폴더구조 구현 - recursive component

by jaewooojung 2024. 3. 5.

ReactJS


*리액트 프로젝트의 폴더구조에 관한 것이 아닌, 리액트로 폴더구조를 구현하는 방법에 관한 글입니다.

 

폴더구조(Folder structure)

 

visual studio code folder explorer

 

 

데이터

폴더구조라 함은 폴더와 파일로 이루어진 데이터의 집합입니다. 운영체제의 폴더 탐색기를 생각하시면 됩니다.

 

데이터의 기본적인 모양새는 다음과 같습니다.

type File = {
  id: String,
  type: String,
  name: String,
  value: String,
};

type Folder = {
  id: String,
  type: String,
  name: String,
  children: Array<File | Folder>,
};

 

이 구조에서의 파일과 폴더의 차이점은 파일은 하위에 더 이상 출력할 데이터가 없고, 폴더에는 children 프로퍼티에 다시 파일 혹은 폴더로 이루어진 배열을 가진다는 점입니다.

 

구현

위의 데이터 타입을 바탕으로 더미 데이터를 생성한 후 구현해 보겠습니다.

const folder1 = {
  id: "folder1",
  type: "folder",
  name: "i am folder1",
  children: [
    {
      id: "file1",
      type: "file",
      name: "i am file1",
      value: "...",
    },
    {
      id: "folder2",
      type: "folder",
      name: "i am folder2",
      children: [
        {
          id: "file2",
          type: "file",
          name: "i am file2",
          value: "...",
        },
        {
          id: "file3",
          type: "file",
          name: "i am file3",
          value: "...",
        },
        {
          id: "folder3",
          type: "folder",
          name: "i am folder3",
          children: [
            {
              id: "file4",
              type: "file",
              name: "i am file4",
              value: "...",
            },
          ],
        },
      ],
    },
  ],
};

 

depth가 4까지 내려가있는 구조입니다.

folder1이 루트 폴더이고, children 프로퍼티에 파일과 폴더들이 들어 있습니다.

 

먼저 루트 폴더의 name을 출력해 봅니다.

export default function App() {
  return <div>{folder1.name}</div>;
}

루트 폴더 이름

 

다음 depth에는 file1과 folder2가 존재합니다. 이들의 name을 출력하기 위해서 children 배열을 순회합니다.

export default function App() {
  return (
    <div>
      <div>{folder1.name}</div>
      {folder1.children.map((v) => {
        return (
          <div key={v.id}>
            <div>{v.name}</div>
          </div>
        );
      })}
    </div>
  );
}

 

depth 2

 

여기서 file1은 최하위 노드이기 때문에 더 이상 출력할 데이터가 남아있지 않지만, folder2에는 아직 children이 남아 있습니다.

 

다음 계층의 존재 여부는 1) 출력되는 데이터의 type이 folder이고 2) (생략가능) children의 길이가 1 이상인지로 판단할 수 있습니다. 리액트에게 다음 계층의 존재 여부를 알려주기 위해서 데이터를 확인한 후

 

데이터가 남아 있다면 JSX를 반환하며 출력해 줍니다.

export default function App() {
  return (
    <div>
      <div>{folder1.name}</div>
      {folder1.children.map((v) => {
        return (
          <div key={v.id}>
            <div>{v.name}</div>
            {v.type === "folder" &&
              v.children.length > 0 &&
              v.children.map((v2) => {
                return (
                  <div key={v2.id}>
                    <div>{v2.name}</div>
                  </div>
                );
              })}
          </div>
        );
      })}
    </div>
  );
}

depth 3

 

folder2의 children인 file2, file3 folder3가 출력되었습니다. 이제 folder3 안에 있는 file4를 출력해 주면 모든 데이터의 출력이 완료됩니다. 한번 더 다음 계층의 존재 여부를 확인한 후 출력해 줍니다.

export default function App() {
  return (
    <div>
      <div>{folder1.name}</div>
      {folder1.children.map((v) => {
        return (
          <div key={v.id}>
            <div>{v.name}</div>
            {v.type === "folder" &&
              v.children.length > 0 &&
              v.children.map((v2) => {
                return (
                  <div key={v2.id}>
                    <div>{v2.name}</div>
                    {v2.type === "folder" &&
                      v2.children.length > 0 &&
                      v2.children.map((v3) => {
                        return (
                          <div key={v3.id}>
                            <div>{v3.name}</div>
                          </div>;
                        )
                      })}
                  </div>
                );
              })}
          </div>
        );
      })}
    </div>
  );
}

 

depth 4

 

다음으로 눈으로 계층을 확인할 수 있도록 각 계층에 paddingLeft 값을 적용해 주겠습니다.

 

paddingLeft값은 ++ 구문을 통해 계층이 내려갈수록 증가합니다.

export default function App() {
  let paddingLeft = 10;
  return (
    <div>
      <div>{folder1.name}</div>
      {folder1.children.map((v) => {
        return (
          <div key={v.id} style={{ paddingLeft: paddingLeft++ }}>
            <div>{v.name}</div>
            {v.type === "folder" &&
              v.children.length > 0 &&
              v.children.map((v2) => {
                return (
                  <div key={v2.id} style={{ paddingLeft: paddingLeft++ }}>
                    <div>{v2.name}</div>
                    {v2.type === "folder" &&
                      v2.children.length > 0 &&
                      v2.children.map((v3) => {
                        return (
                          <div key={v3.id} style={{ paddingLeft: paddingLeft++ }}>
                            <div>{v3.name}</div>
                          </div>
                        );
                      })}
                  </div>
                );
              })}
          </div>
        );
      })}
    </div>
  );
}

 

paddingLeft 적용

 

모든 데이터가 출력되었습니다.

 

눈치채셨겠지만, 이렇게 매번 반복문에서 다음 계층의 존재 여부를 확인하려면 코드가 길고 읽기 힘들어질 뿐만 아니라, 폴더의 depth에 관한 정보를 따로 관리하지 않으면 어느 depth까지 children의 존재 여부를 판단해야 할지 알 수 없으므로 애초에 출력이 불가능에 가깝습니다.

 

 

 

재귀 컴포넌트(Recursive Component)

재귀 컴포넌트를 활용하면 다음 계층의 존재에 대한 확인과 그 출력을 컴포넌트에게 맡김으로써 depth에 관한 정보가 없어도 폴더 구조를 출력할 수 있습니다.

function RecursiveComp(props) {
  const { rowData } = props;
  return (
    <div>
      <div>{rowData.name}</div>
      {rowData.type === "folder" &&
        rowData.children.length > 0 &&
        rowData.children.map((v) => {
          return <RecursiveComp key={v.id} rowData={v} />;
        })}
    </div>
  );
}

 

폴더와 파일 모두 name을 출력해 주는 것은 동일하므로 먼저 name을 출력한 후, prop으로 받은 데이터가 폴더이면 children 배열을 순회하면서 자기 자신을 다시 호출해 줍니다. 이때 child에 대한 다음 계층의 존재 여부는 호출되는 각각의 재귀 컴포넌트가 판단하게 됩니다.

 

재귀 컴포넌트가 준비되었다면 App 컴포넌트에서는 재귀컴포넌트를 호출하면서 루트 폴더를 전달해 주면 됩니다.

export default function App() {
  return <RecursiveComp rowData={folder1} />;
}

 

재귀 컴포넌트 출력 결과

 

 

paddingLeft의 값은 재귀 컴포넌트 안에서 다음 계층으로 넘어갈 때마다 1씩 더해줍니다.

function RecursiveComp(props) {
  const { rowData, paddingLeft } = props;
  return (
    <div style={{ paddingLeft }}>
      <div>{rowData.name}</div>
      {rowData.type === "folder" &&
        rowData.children.length > 0 &&
        rowData.children.map((v) => {
          return <RecursiveComp key={v.id} rowData={v} paddingLeft={paddingLeft + 1} />;
        })}
    </div>
  );
}

 

export default function App() {
  return <RecursiveComp rowData={folder1} paddingLeft={10} />;
}

재귀 컴포넌트 출력 결과 paddingLeft 적용

 

 

끝.

수고하셨습니다.



        
답변을 생성하고 있어요.