중첩된 객체로부터 트리를 생성하고, 트리의 자손 수 나타내기

이나리·2022년 7월 15일
0

문제 상황

이 예제는 모던 자바스크립트 튜토리얼 과제입니다.

중첩된 객체의 데이터로 ul·li 리스트를 생성하는 createTree 함수를 만들어 보세요.

const data = {
  "Fish": {
    "trout": {},
    "salmon": {}
  },

  "Tree": {
    "Huge": {
      "sequoia": {},
      "oak": {}
    },
    "Flowering": {
      "apple tree": {},
      "magnolia": {}
    }
  }
};

코드 형식:

let container = document.getElementById('container');
createTree(container, data); // container 요소 내에 트리를 생성합니다.

내용이 없는 <ul></ul> 처럼 ‘불필요한’ 요소가 트리에 존재해서는 안된다는 점을 참고하세요.

문제 해결 과정

코드 요구사항에 자식 노드가 없는 요소는 트리에 존재하면 안된다고 되어 있으므로, 전달받는 데이터가 빈 객체가 아닐 때만 리스트를 생성할 수 있습니다.

따라서, 전달받는 객체가 빈 객체인지 확인하는 작업이 필요합니다.
Object.keys(obj) 는 객체에 정의된 프로퍼티 키로 구성된 새로운 배열을 만들어주기 때문에, 이 값의 길이가 0 보다 크다면 해당 객체는 프로퍼티가 존재함을 알 수 있습니다.

const objKeys = Object.keys(data);

if (objKeys.length > 0) {
    // 코드 작성....
}

이제 빈 객체는 모두 걸러냈으니, 해당 배열은 프로퍼티가 무조건 존재하겠죠?

배열에 프로퍼티가 할당되어 있기 때문에, 이를 반복하여 <li></li> 요소의 자식 노드로 할당해주고 이를 <ul> 요소에 추가해주면 하나의 리스트를 만들 수 있습니다.

const objKeys = Object.keys(data);

if (objKeys.length > 0) {
  const ul = document.createElement('ul');
  objKeys.forEach(key => {
    const li = document.createElement('li');
    li.textContent = key;
    ul.append(li);
  });
  return ul; // 리스트를 감싸는 ul 요소 리턴.
}

하지만, 아직 중첩된 객체에까지는 접근하지 못했습니다.

중첩된 객체도 마찬가지로 위와 같은 작업이 필요하기 때문에, 코드를 반복하는 작업이 필요할 것 같습니다. 위의 코드를 재사용하기 위해, 함수로 일단 변경해보겠습니다.

function createListTree (data: any) {
  const objKeys = Object.keys(data);

  if (objKeys.length > 0) {
  	const ul = document.createElement('ul');
    objKeys.forEach(key => {
      const li = document.createElement('li');
      li.textContent = key;
      ul.append(li);
    });
    return ul;
  }
};

이제 함수 안에서 같은 함수를 반복하면 중첩된 객체에도 동일한 작업을 실행할 수 있을 겁니다.

그런데 함수를 어디에 놓아야 할까요?
중첩된 객체는 이미 생성된 <li></li> 의 자식 요소인 <ul></ul> 리스트로 생성될 것입니다.
그렇다면, 상위에 생성된 <li></li> 가 그의 부모 요소인 ul 로 합쳐지기 전에 실행한다면, 중첩된 객체의 <ul></ul> 리스트가 부모 요소 li 의 자식 노드로 추가될 것입니다.

(사실 순서를 바꿔서 부모 요소에 먼저 추가하고, 중첩된 객체에서 가져온 ul 을 추가해도 상관없지만, 완성된 <li> 요소로 추가하는 것이 의미적으로 더 낫다고 판단했습니다.)

말로 설명하니 복잡한 것 같아, 일단 먼저 코드로 구현해보도록 하겠습니다.

function createListTree (data: any) {
  const objKeys = Object.keys(data);

  if (objKeys.length > 0) {
  	const ul = document.createElement('ul');
    objKeys.forEach(key => {
      const li = document.createElement('li');
      li.textContent = key;
      // createListTree 함수를 실행해야 하는 위치
      const childrenUl = createListTree(date[key]);
      if (childrenUl) {
        li.append(childrenUl);
      }
      ul.append(li);
  	});
    return ul;
  }
};

중첩된 객체의 경우, 프로퍼티마다 접근하는 것이 필요하기 때문에 createListTree 함수의 매개변수에 대괄호 표기법을 이용해 프로퍼티의 값을 전달했습니다.

중첩된 객체가 빈 객체가 아니라면, 리스트를 가진 ul 요소를 리턴하기 때문에 이를 부모 요소인 li 의 자식 노드로 추가할 수 있습니다.

이제 몇 단계의 중첩 단계를 가진 객체이던, 객체를 순회하여 리스트 요소를 만들 수 있습니다.

이제 마지막으로 완성된 하나의 ul 을 렌더링하는 작업이 필요합니다.
코드 요구사항에 보면, container 라는 요소를 전달하여 이 요소에 리스트를 렌더링하라고 되어 있으니, 요소를 렌더링할 함수를 추가하겠습니다.

이 함수는 요소가 렌더링될 부모 요소와, 렌더링할 요소를 매개변수로 갖습니다.

const data = {
  "Fish": {
    "trout": {},
    "salmon": {}
  },

  "Tree": {
    "Huge": {
      "sequoia": {},
      "oak": {}
    },
    "Flowering": {
      "apple tree": {},
      "magnolia": {}
    }
  }
};

function createListTree (data: any) {
  // 코드 생략
}

function renderListTree (container: Element, element: Element) {
  container.append(element);
};

const root = document.querySelector('#root'); // 다음 요소가 있다고 가정함.
const list = createListTree(data);

if (root) {
  if (list) {
    renderListTree(root, list);
  }
}

추가 문제

이 예제는 모던 자바스크립트 튜토리얼 과제입니다.

중첩된 ul·li 로 이루어진 트리가 있습니다.
<li> 가 가진 자손 요소들의 수를 표시하는 코드를 작성해 보세요. (자식이 없는 노드는 생략하세요.)

위의 상황에, 이 문제를 추가적으로 덧붙여보겠습니다.

문제 해결 과정

<li> 요소가 가진 모든 <li> 요소를 파악하기 위해, 먼저 해당 요소를 모두 검색합니다.
검색이 끝나면, 각각의 <li> 요소 안에 <li> 요소가 몇개 있는지 파악하기 위해 또 검색해야 합니다.

특정 요소에 대한 검색을 2번해야 합니다. 한번은 전체 위치에서, 한번은 전체 위치에서 찾은 요소에서.

// 코드 생략...

if (root) {
  if (list) {
    const allLiElement = list.querySelectorAll('li');
    Array.from(allLiElement).forEach(li => {
      const childrenLiLength = li.querySelectorAll('li').length;
    });
  }
}

querySelectorAll 로 가져온 요소 리스트는 실제 배열이 아니기 때문에, 순회하려면 for of 문을 사용해야 합니다.
저는 편리하게 배열 메서드를 사용하고자, Array.from 을 이용해 해당 리스트를 배열로 변경했습니다.

이렇게 찾은 각 <li> 요소에는 자식 <li> 요소가 있을 수도 있고, 없을 수도 있습니다.

코드의 요구사항에 자식 노드가 없는 경우에는, 개수 표기를 생략하라고 되어 있으니 이를 표기하지 않도록 조건을 추가합니다.

// 코드 생략...

if (root) {
  if (list) {
    const allLiElement = list.querySelectorAll('li');
    Array.from(allLiElement).forEach(li => {
      const childrenLiLength = li.querySelectorAll('li').length;
      if (childrenLiLength && li.firstChild) {
        li.firstChild.textContent += ` [${childrenLiLength}]`;
      }
    });
    renderListTree(container, list);
  }
}

이제 요구사항대로, 갖고 있는 자식 <li> 요소의 개수를 표시할 수 있습니다.

(타입스크립트 사용으로 인해, null 로 타입이 체크되는 것이 많아 if 문을 많이 사용하다보니, 코드가 조금 길어졌네요.)

전체 코드 링크

마치며

1번 문제를 해결하는데 많은 시간이 소요되긴 했는데, 해답을 보지 않고 최대한 제 생각을 담아 코드를 구현하려고 노력했습니다. 문제에서는 2개의 과정을 모두 해보도록 권장했지만, 일단은 1가지 밖에 성공하지 못했습니다.

코드를 모두 구현하고 해답을 확인한 뒤에는, 해답의 좋은 부분을 제 코드로 가져와 추가하였습니다.
가져온 부분은 트리를 생성하는 코드와, 렌더하는 코드를 각각의 함수로 분리하는 과정입니다.

또한, 저는 유사배열을 반복할 때 무조건 배열로 변경 후 처리하려는 습관이 있는데, 해답에서는 굳이 배열로 변경하지 않고, for of 문을 사용하여 순회합니다. 이러한 점은 참고하여 나중에 어떤 방법이 효율적일지 한번 고려해봐야 할 것 같습니다.

그 외에도 보통 반복문에서 조건을 추가할 때, 저의 경우에는 보통 조건에 해당할 때 어떤 코드를 실행하라는 식으로 많이 작성했는데, 해답지에는 continue 문을 이용해 조건에 해당하지 않는 경우에는 코드 실행을 생략하고 다음 반복으로 넘어가도록 하는 코드를 많이 사용한 것을 확인할 수 있었는데요.

개인적으로 조건문과 return 문의 조합을 무지성으로 사용하는 경향이 있었는데, continue 문을 사용하니 if 문 안에서 코드를 작성할 필요가 없어지고, 중괄호 표기 같은 것들을 줄여주기 때문에 제가 작성한 코드보다 훨씬 간결한 코드를 작성할 수 있었습니다.

0개의 댓글