๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

ํ”„๋ก ํŠธ์—”๋“œ

building a File Tree UI given a list of File objects

728x90

 

์˜ค๋Š˜์€ ์ฝ”๋”ํŒจ๋“œ์˜ ๋ฆฌ์•กํŠธ ๋ผ์ด๋ธŒ์ฝ”๋”ฉ ์˜ˆ์ œ ๋ฌธ์ œ๋ฅผ ํ•œ๋ฒˆ ํ’€์–ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค! ใ…Žใ…Ž

๋ฌธ์ œ ์ƒํ™ฉ 

 

Today, we are tasked with building a File Tree UI given a list of File objects, consisting of a path and the contents of the file. Your solution must:

 

  • display files in a nested structure, with entries for each folder and file
  • be able to handle arbitrarily-deep file structures
  • sort the file tree by the following rules:
    • folders are shown before files
    • all items are sorted alphabetically (case-insensitive)

If you solve this challenge and we still have more time, our follow-on task is to implement the ability to add new files to the file tree.

 

์š”๊ตฌ์‚ฌํ•ญ ๋ถ„์„ 

 

1. nested structure ๋ฅผ ๋ณด์—ฌ์ค„ ๊ฒƒ

๐Ÿ“ src
โ”œโ”€โ”€ App.js
โ”œโ”€โ”€ ๐Ÿ“ components
โ”‚   โ””โ”€โ”€ Button.js
โ””โ”€โ”€ ๐Ÿ“ utils
    โ””โ”€โ”€ helpers.js
๐Ÿ“ public
โ””โ”€โ”€ index.html

 

2. arbitrarily-deep file structures ( ์–ผ๋งˆ๋“ ์ง€ ๊นŠ์ด๊ฐ€ ๊นŠ์–ด์งˆ ์ˆ˜ ์žˆ๋Š” ํด๋” ๊ตฌ์กฐ ) ๋„ ์ž˜ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•œ๋‹ค.

3.  file tree ๋ฅผ ๋‹ค์Œ ๊ธฐ์ค€์œผ๋กœ ์ •๋ ฌํ•œ๋‹ค.

- ํด๋”๋Š” ํŒŒ์ผ ์ด์ „์— ๋ณด์—ฌ์ ธ์•ผํ•œ๋‹ค.

- ๋ชจ๋“  ์•„์ดํ…œ๋“ค์€ ์•ŒํŒŒ๋ฒณ์ˆœ์œผ๋กœ ์ •๋ ฌ๋˜์–ด์•ผํ•œ๋‹ค.

 

๊ตฌํ˜„ ์•„์ด๋””์–ด

ํŒŒ์ผ ์‹œ์Šคํ…œ์—์„œ ๊ตฌ์กฐ๊ฐ€ ํŠธ๋ฆฌ ์ž๋ฃŒ๊ตฌ์กฐ๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค๋Š” ๊ฒƒ์€ ์šด์˜์ฒด์ œ์—์„œ ๋ฐฐ์›Œ์„œ ์•Œ๊ณ  ์žˆ์—ˆ๋‹ค.

ํŠธ๋ฆฌ๊ฐ€ ํ•˜๋‚˜์˜ ๋ถ€๋ชจ์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ๊ฐœ์˜ ์ž์‹์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ์ด๋‹ˆ, ํŒŒ์ผํŠธ๋ฆฌ๋ฅผ ํ‘œํ˜„ํ•˜๊ธฐ์— ์ ํ•ฉํ•˜๋‹ค.

 

 

์ฃผ์–ด์ง„ ๊ตฌํ˜„์„ ํ•ด๋‚ด๋ฉด ์•„๋งˆ ์ด๋Ÿฐ ๋А๋‚Œ์˜ ํŠธ๋ฆฌ๊ฐ€ ์™„์„ฑ๋  ๊ฒƒ์ด๋‹ค.

์ธํ’‹์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๋Œ๋ฉด์„œ ํŠธ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ์™„์„ฑํ•ด๋‚˜๊ฐ€๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค!

๊ทธ๋Ÿฐ๋ฐ ์—ฌ๊ธฐ์„œ ๋ฌธ์ œ๋Š” ์ฃผ์–ด์ง„ ์ธํ’‹์„ ์ฒ˜๋ฆฌํ•ด์„œ ํŠธ๋ฆฌ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์กฐ๊ธˆ ๊นŒ๋‹ค๋กœ์šธ ์ˆ˜ ์žˆ๋‹ค.

ํŒŒ์ผํŠธ๋ฆฌ ์ƒ์„ฑํ•˜๊ธฐ

 


๋ณดํ†ต์˜ ํŠธ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ ๊ฐ€์žฅ ์ด์ƒ์ ์ธ ์ธํ’‹์€ ๋ถ€๋ชจ -> ์ž์‹ ๊ด€๊ณ„์— ๋Œ€ํ•œ ๊ฐ„์„ ์œผ๋กœ ์ฃผ์–ด์กŒ๋‹ค.

app/src/WidgetList/WidgetList.tsx ๊ฐ€ ์žˆ๋‹ค๊ณ  ํ•˜๋ฉด

 

app->src, src->WidgetList, WidgetList->WidgetList.tsx ๋กœ ์ชผ๊ฐœ์–ด์„œ ๋ถ€๋ชจ - ์ž์‹ ๊ด€๊ณ„๋ฅผ ์—ฐ๊ฒฐํ•ด์ฃผ๋ฉด ๋œ๋‹ค. 

๋ฌผ๋ก  ์ด๋•Œ๋Š” ์ค‘๋ณต๋˜๋Š” ์š”์†Œ๊ฐ€ ๋“ค์–ด์˜ฌ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ด๋ฅผ ์ž˜ ์ฒ˜๋ฆฌํ•ด์ฃผ์–ด์•ผํ•˜๊ณ , ์ฃผ์–ด์ง„ ๊ฐ„์„ ๋“ค์„ ํ•˜๋‚˜์”ฉ ์ฒ˜๋ฆฌํ•˜๋ฉด์„œ ํŠธ๋ฆฌ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

 

type TreeNode = {
  name: string;
  type: 'folder' | 'file';
  children?: TreeNode[]; // folder
  file?:File;
  
};


const isFile = (path) => path.includes('.');

const buildFileTree = (files: File[]): TreeNode => {

  const root: TreeNode = { name: '', type: 'folder', children: [] };

  // building trees ( ํŒŒ์ผ ํ•˜๋‚˜ํ•˜๋‚˜ ๋Œ๋ฉด์„œ ํŠธ๋ฆฌ ๋งŒ๋“ค๊ธฐ )
  for (const file of files) {
    
    var tokens = file.path.split('/');
    let current = root;

    for(let i=0;i<tokens.length;i++){
      let token = tokens[i];
      // current์— ์ด๋ฆ„์ด token ์ธ ์ž์‹์ด ์žˆ๋Š”์ง€ ํ™•์ธ
      let existing = current.children!.find((c)=>c.name ===token);
      if(!existing){
        const type = isFile(token) ? 'file' as const : 'folder' as const;
        const newNode = {
          name :tokens[i], type, ...(isFile(token) ? { file: file } : { children: [] })
        }
        current.children!.push(newNode);
        current = newNode;
      }else{
        current = existing;
      }
    }
  }

  return root;
}

 

๊ทธ ๊ฒฐ๊ณผ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํŠธ๋ฆฌ ์ž๋ฃŒ๊ตฌ์กฐ๊ฐ€ ๋งŒ๋“ค์–ด์ง„๋‹ค!

 

ํŒŒ์ผ ํŠธ๋ฆฌ ๋ Œ๋”๋ง

๊ทธ ๋‹ค์Œ, ๋งŒ๋“  ํŠธ๋ฆฌ๊ตฌ์กฐ๋ฅผ root -> left -> right  ์ˆœ์œผ๋กœ ์žฌ๊ท€์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋ฉด์„œ ๋ Œ๋”๋ง์‹œ์ผœ์ฃผ๋ฉด ๋œ๋‹ค.

ํŒŒ์ผ ํŠธ๋ฆฌ๋ฅผ ์™ผ์ชฝ๋ถ€ํ„ฐ ์ฐจ๊ทผ์ฐจ๊ทผ DFS ํƒ์ƒ‰์„ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉด ํŽธํ•˜๋‹ค! ๊ฒฐ๊ตญ ์ „์œ„์ˆœํšŒ์ธ๊ฒƒ!

 

 

ํŠธ๋ฆฌ์˜ ๊ฐ ๋…ธ๋“œ๋ฅผ ํƒ์ƒ‰ํ•˜๋ฉด์„œ 

 

1. ํŒŒ์ผ์ด๋ผ๋ฉด ๋ฐ”๋กœ ๋ Œ๋”๋ง

2. ํด๋”๋ผ๋ฉด ๊ฐ ์ž์‹๋…ธ๋“œ๋“ค์„ root ๋กœ ํ•˜์—ฌ ๋ณธ๊ณผ์ • ๋‹ค์‹œ ๋ฐ˜๋ณต (์žฌ๊ท€)

 

export const FileNode = ({root,depth}) =>{

  if(root.type =='file'){
    console.log("file",root);
    return  <Box display="flex" justifyContent={"flex-start"}
   ><span style={{marginLeft:depth*20-15}}/><FileRow file={root.file} /></Box>
  }
  else if(root.type=='folder'){

    console.log(root.children);
   return  (<Box><span style={{marginLeft:depth*20}}> ใ„ด ๐Ÿ“ {root.name}</span>
    <Box />
        {root.children?.map((c)=><FileNode depth={depth+1} root={c} key={c.path}/>)} 
    </Box>)
  }

  return null;

}

export const FilePane = () => {
  const { files } = useWorkspaceContext()

  const root = buildFileTree (files);
  console.log(root)


  return (<Box>
      <Box p={1}>
        <Typography variant="h6">Files</Typography>
        {root.children?.map((c:TreeNode)=><FileNode depth={0} root={c} key={c.name}/>)} 
        </Box>
      </Box>)
}

 

์—ฌ๊ธฐ์„œ ํฌ์ธํŠธ๋Š” ํด๋”์˜ ๊นŠ์ด์— ๋น„๋ก€ํ•ด์„œ ์™ผ์ชฝ ๋งˆ์ง„ ๊ฐ’์„ ์ฃผ์–ด์•ผํ•œ๋‹ค๋Š” ๊ฒƒ! ๋”ฐ๋ผ์„œ ๋งค ์žฌ๊ท€ํ˜ธ์ถœ์— depth ๋ผ๋Š” ์ธ์ž๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ 

depth*20 ๋งŒํผ ์•ž์„ ๋น„์›Œ๋†“๋„๋ก ํ–ˆ๋‹ค. 

 

 

์ตœ์ข… ๊ตฌํ˜„ ๊ฒฐ๊ณผ 

ํŠธ๋ฆฌ ํŒŒ์ผ ๊ณ„์ธต ๊ตฌ์กฐ๋กœ ๋‚˜ํƒ€๋‚œ ๋ชจ์Šต!

 

 

์ถ”๊ฐ€๊ธฐ๋Šฅ

๋ฉ”์ธ ๊ธฐ๋Šฅ์„ ์™„์„ฑํ•œ ์ดํ›„์—๋Š” ์ถ”๊ฐ€ ๊ธฐ๋Šฅ์„ ์™„์„ฑํ•ด ๋ณผ ๊ฒƒ์ด๋‹ค.

 

1. nested structure ์„ ์ •๋ ฌ๋œ ์ƒํƒœ๋กœ ๋ณด์—ฌ์ค„ ๊ฒƒ

sort the file tree by the following rules:
folders are shown before files
all items are sorted alphabetically (case-insensitive)

 

=> ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•˜๋ฉด์„œ, ๋ชจ๋“  ๋ถ€๋ชจ๋…ธ๋“œ๋“ค์— ๋Œ€ํ•˜์—ฌ ์ž์‹ ๋…ธ๋“œ๋“ค์„ ์•ŒํŒŒ๋ฒณ ์ˆœ์œผ๋กœ ์ •๋ ฌํ•ด์ฃผ๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค!

 

2. ํŒŒ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ(add File) ๊ธฐ๋Šฅ 

 

=> ์ „์ฒด ํŒŒ์ผ ๋ชฉ๋ก์„ state ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , HTML input ํƒœ๊ทธ๋ฅผ ํ†ตํ•ด์„œ ๋กœ์ปฌ ์ปดํ“จํ„ฐ๋กœ๋ถ€ํ„ฐ ํŒŒ์ผ์„ ๋ฐ›์•„์˜จ ํ›„,

ํ•ด๋‹น ํŒŒ์ผ์— ์ ‘๊ทผํ•˜์—ฌ ์ œ๋ชฉ, ๋‚ด์šฉ ๋“ฑ์„ ์•Œ์•„๋‚ด์–ด ์ „์ฒด ํŒŒ์ผ ๋ชฉ๋ก์— ์ถ”๊ฐ€์‹œํ‚ค๋ฉด ๋  ๊ฒƒ ๊ฐ™๋‹ค! 

 

์ •๋ ฌํ•˜๊ธฐ

ํŒŒ์ผํŠธ๋ฆฌ์˜ root ๋ถ€ํ„ฐ, ๊ฐ children ์„ ๋ชจ๋‘ ๋ฐฉ๋ฌธํ•˜๋ฉด์„œ

 

1) children์ด ์žˆ๋‹ค๋ฉด,

์•ž์„œ ์žˆ๋Š”๊ฒŒ ํด๋”๋ผ๋ฉด -> ์ˆœ์„œ ๊ทธ๋Œ€๋กœ ์œ ์ง€๋ฅผ ์œ„ํ•ด -1 ๋ฐ˜ํ™˜

์•ž์„œ ์žˆ๋Š”๊ฒŒ ํŒŒ์ผ์ด๋ผ๋ฉด -> ์ˆœ์„œ๋ฅผ ๋ฐ”๊พธ๊ธฐ ์œ„ํ•ด 1 ๋ฐ˜ํ™˜

๊ทธ๋ฆฌ๊ณ  ๊ฐ children ์— ๋Œ€ํ•˜์—ฌ sortTree() ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ํ˜ธ์ถœํ•ด์ค€๋‹ค. ํ˜„์žฌ ๋…ธ๋“œ ํ•˜์œ„์— ์žˆ๋Š” ์–ด๋–ค ํด๋” ๋‚ด๋ถ€์˜ ํŒŒ์ผ๋“ค๋„  ๋ชจ๋‘ ์ •๋ ฌ๋  ํ•„์š”๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

2) children์ด ์—†๋‹ค๋ฉด, ์•„๋ฌด์ผ๋„ ์ผ์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค.

 

const sortTree = (root: TreeNode)=>{

  if(root.children){
    root.children.sort((a, b) => {
      if(a.type != b.type){
        return a.type==='folder'?-1:1;
      }
      return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
    });

    root.children.forEach((c)=>{
      sortTree(c);
    })

  }
  
}

ํŒŒ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ

 

์šฐ์„ , ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ ์œ„ํ•œ input ๊ณผ ์ œ์ถœ์„ ์œ„ํ•œ button ํƒœ๊ทธ๋ฅผ ๊ฐ๊ฐ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

      <input type="file" ref={inputRef}/>
          <button onClick={addFile}>add File</button>

 

๊ทธ ๋‹ค์Œ, ๊ธฐ์กด file ๋ชฉ๋ก์„ contextAPI ๋กœ ์ „์—ญ์— ๋ฟŒ๋ฆฌ๊ณ , addFile ํ˜ธ์ถœ์‹œ์— file ์ •๋ณด๋ฅผ ๋ฐ›์•„์™€์„œ allFiles ์— ์ถ”๊ฐ€ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

export const FilePane = () => {
  const { allFiles,setAllFiles } = useWorkspaceContext();

  const root = buildFileTree (allFiles);
  sortTree(root);
  console.log(root);

  const inputRef = useRef(null);

  const addFile = async (e) => {

    console.log("addFile");
    //e.preventDefault();
    const files = inputRef.current.files;
    const file = files[0];
    console.log(file);
    const path = file.name;
    const contents : string = await file.text();
  
    const newFile : File = {path,contents};
  
    setAllFiles([...allFiles,newFile]);
    
  
  }


  return (<Box>
      <Box p={1}>
        <Typography variant="h6">Files</Typography>
       
          <input type="file" ref={inputRef}/>
          <button onClick={addFile}>add File</button>
  
        {root.children?.map((c:TreeNode)=><FileNode depth={0} root={c} key={c.name}/>)} 
        </Box>
      </Box>)
}

 

๋˜ํ•œ, ๋‚˜๋Š” ํ…Œ์ŠคํŠธ์šฉ์œผ๋กœ .cpp ํŒŒ์ผ์„ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ ์ด๋ ‡๊ฒŒ ์ƒˆ๋กญ๊ฒŒ ์˜ฌ๋ผ๊ฐ„ ํŒŒ์ผ ํ™•์žฅ์ž๋„ ์—๋””ํ„ฐ ์ƒ์—์„œ ์˜ˆ์˜๊ฒŒ ๋ณด์ผ ์ˆ˜ ์žˆ๋„๋ก,

์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.

  else if(['.c','.cpp'].some((extension)=>filePath.endsWith(extension))){
    return 'c'
  }

 

์ „์ฒด์ ์ธ ๋ชจ์Šต์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค!

import React from 'react'
import { Box } from '@mui/material'
import { useWorkspaceContext } from '../Workspace/WorkspaceContext'
import MonacoEditor from "@monaco-editor/react";
import * as monaco from 'monaco-editor'

// in lieu of having actual Language Workers to determine the lang from a file path, we'll hard-code some handlers here
const getLanguageFromFilePath = (filePath: string) => {
  if (filePath.endsWith('.html')) {
    return 'html'
  } else if (filePath.endsWith('.css')) {
    return 'css'
  } else if (['.js', '.jsx', '.ts', '.tsx'].some((extension) => filePath.endsWith(extension))) {
    return 'javascript'
  }
  else if(['.c','.cpp'].some((extension)=>filePath.endsWith(extension))){
    return 'c'
  }

  return 'plaintext'
}

export const Editor = () => {
  const { activeFile } = useWorkspaceContext()
  
  if (activeFile == null) return null

  return (
    <Box id='editor' flex={1}>
      <MonacoEditor 
        value={activeFile?.contents} 
        language={getLanguageFromFilePath(activeFile?.path)}
      />
    </Box>
  )
}

 

 

์ตœ์ข… ๊ฒฐ๊ณผ

 

๋กœ์ปฌ์—์„œ ํŒŒ์ผ์„ ์„ ํƒํ•˜๊ณ  ์ด๋ฅผ ์—…๋กœ๋“œํ•˜์—ฌ ์—๋””ํ„ฐ ์ฐฝ์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค!