25- React 파일 관리자 25장: 커널 트리

그래서 우리는 성공적으로 서버에 새 디렉토리를 생성하는 것을 멈췄습니다. 이제 트리 기반 커널을 만들어 내부에 모든 노드를 주입하겠습니다.

하지만 그 전에 코드를 조금 정리합시다.

코드 청소



주요 구성 요소 함수FileManager.tsx로 이동하고 다음 코드로 업데이트합니다.

import { Grid } from "@mantine/core";
import Content from "app/file-manager/components/Content";
import LoadingProgressBar from "app/file-manager/components/LoadingProgressBar";
import Sidebar from "app/file-manager/components/Sidebar";
import Toolbar from "app/file-manager/components/Toolbar";
import { KernelContext } from "app/file-manager/contexts";
import Kernel from "app/file-manager/Kernel";
import { useEffect, useRef } from "react";
import { BodyWrapper } from "./FileManager.styles";
import { FileManagerProps } from "./FileManager.types";

export default function FileManager({ rootPath }: FileManagerProps) {
  const { current: kernel } = useRef(new Kernel(rootPath as string));

  // load root directory
  useEffect(() => {
    if (!rootPath) return;

    kernel.load(rootPath);
  }, [rootPath, kernel]);

  return (
    <KernelContext.Provider value={kernel}>
      <LoadingProgressBar />
      <Toolbar />
      <BodyWrapper>
        <Grid>
          <Grid.Col span={3}>
            <Sidebar />
          </Grid.Col>
          <Grid.Col span={9}>
            <Content />
          </Grid.Col>
        </Grid>
      </BodyWrapper>
    </KernelContext.Provider>
  );
}

FileManager.defaultProps = {
  rootPath: "/",
};


여기서 우리가 한 것은 정의된 모든 states이 여기에서 더 이상 필요하지 않기 때문에 제거한 것이며 모든 상태는 이제 Kernel 클래스 안에 있습니다.

또한 rootPath를 커널 클래스에 전달하고 useEffect 후크 내부의 루트 디렉토리를 로드합니다.

커널 트리



이제 생성자 메서드를 추가하여 rootPath를 수락하고 그것으로 tree를 초기화하겠습니다.

// app/file-manager/Kernel/Kernel.ts
import { Node } from "app/file-manager/Kernel/Node";
...
  /**
   * Constructor
   */
  public constructor(rootPath: string) {
    this.rootPath = rootPath;
  }


이제 KernelTree 클래스 옆에 우리의 Kernel 클래스도 정의해 보겠습니다.

// app/file-manager/Kernel/KernelTree.ts
import Kernel from "./Kernel";
import { Node } from "./Kernel.types";

export default class KernelTree {
  /**
   * Root node
   */
  public root?: Node;

  /**
   * Constructor
   */
  constructor(public kernel: Kernel) {}
}


생성자에 커널을 주입하여 커널에서 직접 모든 메서드/속성을 사용할 수 있습니다.

이제 KernelTree 클래스를 사용하도록 커널 클래스를 업데이트하겠습니다.

// app/file-manager/Kernel/Kernel.ts
import events, { EventSubscription } from "@mongez/events";
import { createDirectory } from "../actions";
import fileManagerService from "../services/file-manager-service";
import { KernelEvents, Node } from "./Kernel.types";
import KernelTree from "./KernelTree";

export default class Kernel {
  ...
  /**
   * Kernel nodes tree
   */
  public tree: KernelTree;

  /**
   * Root node
   */
  public rootNode?: Node;

  /**
   * Constructor
   */
  public constructor(rootPath: string) {
    this.rootPath = rootPath;

    this.tree = new KernelTree(this);
  }
  ...
}


커널 트리의 개념



커널 트리의 개념은 모든 노드를 내부에 주입하는 것이므로 모든 노드에 쉽게 직접 액세스하고 업데이트, 삭제 또는 하위 목록을 업데이트할 수 있습니다.

따라서 우리의 KernelTree 클래스에는 다음과 같은 기능이 있습니다.
  • 노드 설정: 주어진 노드를 트리에 추가하고 노드가 이미 있으면 업데이트합니다.
  • 노드 가져오기: 경로 또는 노드 자체로 노드를 가져옵니다.
  • 노드 삭제: 경로 또는 노드 자체로 노드를 삭제합니다.
  • 부모 노드 가져오기: 지정된 노드의 부모 노드를 가져옵니다.
  • Order Node Children: 지정된 순서에 따라 지정된 노드의 자식을 정렬합니다.
  • 노드 하위를 디렉토리 및 파일로 정의합니다.

  • 이제 KernelTree 클래스 구현을 시작하겠습니다.

    // app/file-manager/Kernel/KernelTree.ts
    import Kernel from "./Kernel";
    import { Node } from "./Kernel.types";
    
    export default class KernelTree {
      /**
       * Root node
       */
      public root?: Node;
    
      /**
       * Constructor
       */
      constructor(public kernel: Kernel) {}
    
      /**
       * Set root node
       */
      public setRootNode(root: Node) {
        this.root = root;
        this.kernel.trigger("nodeChange", this.root);
    
        this.prepareNode(this.root);
      }
    }
    


    여기에 setRootNode를 추가하여 내부에 모든 것을 포함할 최상위 노드를 정의하고 prepareNode를 호출하여 노드 자식을 디렉토리 및 파일로 분할하고 알파벳순으로 자식을 이름으로 정렬하는 두 가지 작업을 수행합니다.

    노드 준비 중



    이전에 언급한 것처럼 노드 자식을 디렉토리와 파일로 분할하고 알파벳순으로 이름별로 자식을 정렬합니다.

    // app/file-manager/Kernel/KernelTree.ts
    
    ...
    
      /**
       * Prepare the given node
       */
      public prepareNode(node: Node) {
        if (!node.children) return;
    
        this.reorderChildren(node);
    
        // set children directories
        node.directories = node.children.filter(child => child.isDirectory);
    
        // set children files
        node.files = node.children.filter(child => !child.isDirectory);
      }
    
      /**
       * Reorder node children by child name
       */
      public reorderChildren(node: Node) {
        node.children?.sort((a, b) => {
          if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) return 1;
          if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) return -1;
          return 0;
        });
      }
    


    하지만 Node 유형에는 directoriesfiles 속성이 없으므로 추가해 보겠습니다.

    // app/file-manager/Kernel/Kernel.types.tsx
    /**
     * File Manager node is the primary data structure for the File Manager.
     * It can be a directory or a file.
     * It contains the following properties:
     */
    export type Node = {
      /**
       * Node Name
       */
      name: string;
      /**
       * Node full path to root
       */
      path: string;
      /**
       * Node size in bits
       */
      size: number;
      /**
       * Is node directory
       */
      isDirectory: boolean;
      /**
       * Node children
       * This should be present (event with empty array) if the node is directory
       */
      children?: Node[];
      /**
       * Get children directories
       */
    👉🏻  directories?: Node[];
      /**
       * Get children files
       */
    👉🏻  files?: Node[];
    };
    


    커널 트리 업데이트



    이제 노드가 서버에서 로드되면 트리를 업데이트해야 하므로 load 메서드로 이동하겠습니다.

      /**
       * Load the given path
       */
      public load(path: string): Promise<Node> {
        // trigger loading event
        this.trigger("loading");
    
        return new Promise((resolve, reject) => {
          fileManagerService
            .list(path)
            .then(response => {
              this.currentDirectoryPath = path;
    
              if (response.data.node.path === this.rootPath) {
                👉🏻 this.tree.setRootNode(response.data.node);
                this.rootNode = response.data.node;
              } else {
                👉🏻 this.tree.setNode(response.data.node);
              }
    
              // trigger load event as the directory has been loaded successfully.
              this.trigger("load", response.data.node);
    
              // if the current directory is not as the same loaded directory path,
              // then we'll trigger directory changed event.
              if (response.data.node.path !== this.currentDirectoryNode?.path) {
                this.trigger("directoryChange", response.data.node);
              }
    
              this.currentDirectoryNode = response.data.node;
    
              resolve(this.currentDirectoryNode as Node);
            })
            .catch(reject);
        });
      }
    


    이제 트리 노드 내부에 노드를 전달하면서 트리 노드를 업데이트할 수 있습니다. 따라서 노드가 로드되면 루트 노드인지 확인한 다음 루트 노드를 업데이트하고 그렇지 않으면 로드된 노드만 업데이트합니다.
    setNode 클래스 내부에 KernelTree 메서드를 생성해 보겠습니다.

    // app/file-manager/Kernel/KernelTree.ts
      /**
       * Add the given node to the tree
       */
      public setNode(node: Node) {
        // first find the parent node
        let parentNode = this.parentNode(node);
    
        // if it has no parent, which should not happen, then mark the root as parent
        if (!parentNode) {
          parentNode = this.root;
        }
    
        // if there is no parent, then do nothing and just return
        if (!parentNode) return;
    
        // a flag to determine if the given node was already existing but has been changed
        let nodeHasChanged = false;
        // a flag to determine if the parent node is changed
        let parentHasChanged = false;
    
        // now check if the node already exists in its parent
        if (this.parentHas(parentNode, node)) {
          // if it exists, replace it
          parentNode.children = parentNode.children?.map(child => {
            if (child.path === node.path) {
              if (this.nodeHasChanged(child, node)) {
                nodeHasChanged = true;
                parentHasChanged = true;
              }
    
              return node;
            }
    
            return child;
          });
        } else {
          // it means the node does not exist in the parent, then push it to the parent's children
          parentNode?.children?.push(node);
          this.kernel.trigger("newNode", node);
          parentHasChanged = true;
          // prepare the node
          this.prepareNode(node);
        }
    
        // this will be only triggered if the node has changed
        if (nodeHasChanged) {
          this.prepareNode(node);
          this.kernel.trigger("nodeChange", node);
        }
    
        // this will be only triggered if the parent node has changed
        if (parentHasChanged) {
          this.prepareNode(parentNode);
          // as the parent node has changed thus the root node will be marked as changed as well
          // we may later make it recursive to mark all the parent nodes as changed
          this.prepareNode(this.root as Node);
    
          this.kernel.trigger("nodeChange", parentNode);
          this.kernel.trigger("nodeChange", this.root);
        }
      }
    


    코드는 자명합니다. 노드가 부모에 이미 있는지 확인하고, 있으면 교체하고, 그렇지 않으면 부모의 자식으로 푸시합니다.

    다음 메서드도 추가해 보겠습니다.
  • parentNode 주어진 노드
  • 의 부모 노드를 가져옵니다.
  • parentHas 상위 노드에 지정된 노드가 있는지 확인
  • nodeHasChanged 지정된 노드가 변경되었는지 확인

  • // app/file-manager/Kernel/KernelTree.ts
    
      /**
       * Check if the given parent has the given node
       */
      public parentHas(parent: Node, node: Node): boolean {
        return parent.children?.some(child => child.path === node.path) ?? false;
      }
    
      /**
       * Get parent node
       */
      public parentNode(node: Node): Node | undefined {
        return this.findNode(this.getParentPath(node.path));
      }
    
      /**
       * Find node for the given path recursively in the tree
       */
      public findNode(path: string): Node | undefined {
        // loop starting from the tree root
        const currentNode = this.root;
    
        const findNode = (node?: Node): Node | undefined => {
          if (node?.path === path) {
            return node;
          }
    
          if (!node?.children) return undefined;
    
          for (const child of node.children) {
            const foundNode = findNode(child);
    
            if (foundNode) return foundNode;
          }
        };
    
        return findNode(currentNode);
      }
    
      /**
       * Check if the given node has been changed
       */
      public nodeHasChanged(oldNode: Node, newNode: Node): boolean {
        return JSON.stringify(oldNode) !== JSON.stringify(newNode);
      }
    
      /**
       * Get the parent path of the given path
       */
      protected getParentPath(path: string): string {
        if (!path) return "/";
    
        // get the parent path by splitting the path and removing the last item
        return path.split("/").slice(0, -1).join("/");
      }
    


    마지막으로 할 일은 커널 이벤트를 업데이트하는 것입니다.

    // Kernel.types
    
    /**
     * Kernel events
     */
    export type KernelEvents =
      | "loading"
      | "load"
      | "directoryChange"
      | "nodeChange"
      | "nodeDestroy"
      | "newNode";
    


    노드가 변경될 때 트리거되는 nodeChange, 노드가 파괴될 때 트리거되는 nodeDestroy, 새 노드가 추가될 때 트리거되는 newNode를 추가했습니다.

    다음 장



    다음 장에서는 만들기 디렉터리, 사이드바 및 노드 변경 사항을 감시할 콘텐츠를 향상시킬 것입니다.

    기사 저장소



    Github Repository에서 챕터 파일을 볼 수 있습니다.

    Don't forget the main branch has the latest updated code.



    지금 어디 있는지 말해줘



    이 시리즈를 저와 함께 후속 조치하는 경우 현재 위치와 어려움을 겪고 있는 부분을 알려주시면 최대한 도와드리겠습니다.

    살람.

    좋은 웹페이지 즐겨찾기