개발자 윤찬

웹 개발자 윤찬의 프로필 사진개발자 윤찬
2025. 8. 17.

Leaf - 논코딩툴

React.jsDeveloper
"지속적인 개발"

DREAMMAKER

2년전에 취업을 하기전에 개인프로젝트로 하나 만든게 있다. 논코딩툴인데, DREAMMAKER라는 프로젝트이다. 백엔드 기술 하나도 없이, 오직 프론트에서의 구현과 관리를 통해서 1분만에 내가 원하는 웹사이트를 만들 수 있는 논코딩 툴이다.

DreamMaker - 레포주소

저 프로젝트를 할 때, 주변은 모두 백엔드 개발자를 준비하고 있었고 프론트엔드 개발자를 준비하는 사람이 나 밖에 없었다. 취준을 할 때라 직무를 잘못 선택한 것인가 하는 생각도 들때가 있었고 다른 프론트엔드와 차별점이 없다고 생각하게 되었다. 고민을 많이 했는데 이왕 선택한 직무니 하나를 잘 만들어보고 아쉬워하자는 마음에 시작한 프로젝트이다.

오직 frontend 기술만을 사용해서, 누가 보더라도 감탄할 수 있는 프로젝트를 하나 만들자

이 프로젝트를 만들고나서, 회사들을 지원할때 항상 회사 홈페이지를 저 서비스로 만들어서 제출했다. 또 면접에서 많은 질문을 받았던 것 같다.


경력 1년이 지난 후

프론트엔드 개발자로 경력이 1년이 지나고 사실 아쉬운 점이 하나 있었다. DREAMMKAER에서 사실 놓친 부분이 있었는데

- 모듈화
- 레이아웃이 없어 계층 관계가 형성이 되지 않음

이렇게 두 가지 있었는데, 특히 두번째 레이아웃이 없어 계층 관계가 형성이 되지 않는 경우에서는 심각한 문제였다. 모든 element들이 body의 바로 직속 자식으로 배치가 되기 때문에 실제 코드를 다운받더라도 재사용할 수 없었기 때문이다. 이 문제를 면접에서도 많이 질문 받았었고 피드백으로 받아왔기도 했지만 일과 다른 공부때문에 계속 남겨두고 있었다. 실제 레포에는, refactoring 예정을 적었지만 1년이 넘게 지났다.

뭔가 찝찝한 프로젝트로 두고 미뤄뒀었다.


Leaf라는 이름으로 시작

DreamMaker로 하려 했지만 오글거려서 Leaf로 변경 Leaf - 레포주소

새로 시작하는 느낌으로 디자인도 개선을 했고, 특히 계층을 꼭 넣었다. 트리구조를 기반으로, react element를 생성할 때 실제 동작을 하는 방식을 참고해 적용시켰다.

내가 만든 리액트

{
  id: Id,            
  object: movingObject,   
  children: [],           
}

각 객체는 본인의 고유 ID와 연결된 Fabric 객체, 그리고 자식 노드 배열을 가진다. 자식이 존재하면 동일한 구조를 가지므로, 트리는 재귀적으로 구성된다. 노드를 추가하거나 삭제할 때는 재귀 알고리즘을 사용하여 부모-자식 관계를 유지하면서 트리를 갱신한다. 이 과정에서 Fabric 객체의 위치 변경도 함께 적용되어, 상태 변화가 React의 Fiber 구조처럼 효율적으로 반영된다.

각 계층이 순환을 하면서 부모노드가 자식 노드를 찾아가는 방식으로 적용이 되고, 중간 삽입/삭제/정렬/이동 모두 treeUtils에서 children의 key value를 추가 제거 하는 방식으로 나타난다. 하나의 예시를 보면

해당 코드는, 이동할 노드를 찾아 제거, 지정된 부모의 자식으로 추가, 부모가 없으면 새로 생성의 내부적인 로직으로 레이아웃 계층을 또 추가하는 방식이다.

    const removeNode = (nodes: TreeNode[]): TreeNode[] =>
      nodes
        .map((node) => {
          if (node.id === childId) {
            movingNode = node; // 기존 자식까지 유지
            return null;
          }
          return { ...node, children: removeNode(node.children) };
        })
        .filter(Boolean) as TreeNode[];

    const updatedTree = removeNode(prevTree);

먼저 removeNode 함수는 주어진 트리를 순회하면서 childId와 일치하는 노드를 찾아낸다. 해당 노드를 발견하면 그 노드를 movingNode라는 변수에 저장하고, 반환값에서는 제외시켜 트리에서 잘라낸다. 이 과정에서 노드의 자식까지 함께 보존되기 때문에 구조가 깨지지 않고 그대로 유지된다. 반대로 childId가 아닌 노드를 만나면, 그 노드의 자식 배열을 재귀적으로 탐색하며 같은 제거 과정을 반복한다. 최종적으로 childId가 제거된 새로운 트리가 반환한다.

    const insertNode = (nodes: TreeNode[]): TreeNode[] =>
      nodes.map((node) =>
        node.id === parentId
          ? { ...node, children: [...node.children, movingNode] }
          : { ...node, children: insertNode(node.children) }
      );

그 다음 단계는 잘라낸 노드를 원하는 위치로 붙이는 작업이다. insertNode 함수는 트리를 순회하면서 parentId와 일치하는 노드를 찾는다. 만약 찾으면 해당 노드의 children 배열에 movingNode를 추가해 자식으로 삽입한다. 만약 현재 노드가 parentId가 아니라면 자식들을 재귀적으로 내려가며 계속 탐색을 이어간다.

    const parentExists = JSON.stringify(updatedTree).includes(parentId);

    return parentExists
      ? insertNode(updatedTree)
      : [
          ...updatedTree,
          {
            id: parentId,
            object: layoutObject,
            children: [movingNode],
          },
        ];

마지막으로, 이동시킬 부모 노드가 트리 안에 존재하는지 확인하는 과정이 있다. updatedTree를 문자열로 변환해서 parentId가 포함되어 있는지를 검사하는 방식이다. 만약 부모 노드가 이미 존재한다면 insertNode 함수를 실행해 그 아래에 movingNode를 붙인다. 하지만 부모가 존재하지 않는다면, 트리 최상위 레벨에 새로운 부모 노드를 만들어서 그 안에 movingNode를 자식으로 두게 된다.

이것이 노드를 떼어내고→ 새로운 부모 밑으로 붙이거나 → 부모가 없으면 새로운 부모를 만들어서 넣는 구조이기 때문에 트리 안에서 자유롭게 노드 이동이 가능하다.

결과적으로 canvas에서 인터렉티브하게 계층 삽입이 가능하고, 사이드 패널에서도, 부모의 노드의 이동으로 전체 자식노드들의 포함을 조정할 수 있다.

이게 좋은 이유가, 레이아웃이 생기게 된다면, 상위의 속성이 하위의 모든 속성에 영향을 받는데, 정렬이나, 반응형 그리고 애니메이션등이 모두 하나의 트리거에 발생된다. 또한 최종 export 결과를 보면, 계층이 형성되어 코드를 재사용할 수 있다.


앞으로 계획

취준할 때 만들었던 DREAMMAKER는 프론트엔드 기술만을 활용해 개발한 프로젝트였다. 하지만 지금 실무를 경험해보니 단순히 프론트엔드에만 머무르는 것이 아니라, 하나의 사이클을 끝까지 돌려보고 싶다는 생각이 들었다. 여기서 말하는 원사이클이란 프론트엔드뿐만 아니라 백엔드와 DevOps까지 모두 아우르는 과정이다. 물론 나는 프론트엔드를 주력으로 삼겠지만, 궁극적으로는 하나의 서비스를 처음부터 끝까지 직접 책임지고 만들어낼 수 있는 개발자가 되는 것을 목표로 하고 있다.

마지막으로, 새로 만든 LEAF로 만든 홈페이지를 첨부하고 마무리

Profile Picture

CHAN

과정은 복잡하되, 결과는 단순하게

Thank You for Visiting My Blog, Have a Good Day 😆
ⓒYoonchan Cho