코로 넘어져도 헤딩만 하면 그만

Tree UI를 재귀함수로 만들기 본문

CODE STATES 44

Tree UI를 재귀함수로 만들기

꼬드리 2023. 4. 12. 15:18

 

내 안의 내 안의 내 안의 나.

재귀함수란?

자기 자신을 호출하는 함수. 가장 작게 쪼개졌을 때, 즉 특정 조건에 도달하면 이 반복에서 '탈출'할 수 있는 지점이 있어야 합니다. 그렇지 않으면 무한으로 자신을 불러오다가 Error을 띄우고 맙니다. 

 

1. 주어진 문제를 비슷한 구조의 더 작은 문제로 나눌 수 있는 경우

2. 중첩된 반복문이 많거나 반복문의 중첩 횟수(number of loops)를 예측하기 어려운 경우

 

이런 경우에 재귀 함수를 쓰면 도움이 됩니다. 모든 재귀 함수는 반복문(while 문 또는 for 문)으로 표현할 수 있다고 하네요. 하지만 재귀함수를 쓰면 코드가 훨씬 짧고 가독성이 좋아지는 것을 볼 수 있습니다.

 

 

 

🐥재귀함수를 사용해서 팩토리얼 만들기 예시

function factorial(num) {
  // n-factorial(n!; 엔-팩토리얼) 값을 리턴해야 합니다.
  //n! 은 1부터 n까지 1씩 증가한 모든 값의 곱입니다.
  //factorial(0)은 1로 정의됩니다.
  //factorial(3)= 3*factorial(2);
  if(num===0){
    return 1;
  }
  return num * factorial(num-1);
}

 

 

 

TREE UI 를 재귀함수로 만들기 

이처럼 재귀 함수에 대해서 개념과 사용법을 배웠지만, 사실 존재하는 메서드를 왜 재귀 함수로 또 작성하는지 의문을 품고 있었습니다. 그래서 이번에는 직접 dom을 만들며 실습을 했고, 재귀함수의 편리함과 그 작동 구조에 대해 이해하게 되었브니다. 최종적으로는 아래와 같은 구조의 Tree UI를 만드는 것이 목표였습니다. 

DOM이 트리 구조이기 때문에 재귀함수를 사용하여 화면에 표시하면 좋습니다. 

  <body>
    <ul id="root">
      <li>
        <input type="checkbox" checked />
        <span>음료</span>
        <ul>
          <li>
            <input type="checkbox" />
            <span>콜드브루</span>
            <ul>
              <li>나이트로 콜드 브루</li>
              <li>돌체 콜드 브루</li>
              <li>제주 비자림 콜드 브루</li>
              <li>콜드 브루</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>프라푸치노</span>
            <ul>
              <li>애플 쿠키 크림 프라푸치노</li>
              <li>더블 에스프레소 칩 프라푸치노</li>
              <li>모카 프라푸치노</li>
              <li>피스타치오 크림 프라푸치노</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>블렌디드</span>
            <ul>
              <li>망고 바나나 블렌디드</li>
              <li>딸기 요거트 블렌디드</li>
              <li>자몽 셔벗 블렌디드</li>
              <li>피치 & 레몬 블렌디드</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>티</span>
            <ul>
              <li>라임 패션 티</li>
              <li>민트 블렌드 티</li>
              <li>아이스 유스베리 티</li>
              <li>아이스 캐모마일 블렌드 티</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>주스</span>
            <ul>
              <li>한방에 쭉 감당</li>
              <li>파이팅 청귤</li>
              <li>딸기주스</li>
              <li>도와주 흑흑</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>
        <input type="checkbox" />
        <span>음식</span>
        <ul>
          <li>
            <input type="checkbox" />
            <span>빵</span>
            <ul>
              <li>트러플 미니 스콘</li>
              <li>보늬밤 몽블랑 데니쉬</li>
              <li>고소한 치즈 베이글</li>
              <li>미니 클래식 스콘</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>케이크</span>
            <ul>
              <li>밀당 에그 타르트</li>
              <li>마스카포네 티라미수 케이크</li>
              <li>블루베리 쿠키 치즈 케이크</li>
              <li>부드러운 생크림 카스텔라</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>샌드위치</span>
            <ul>
              <li>애플 까망베르 샌드위치</li>
              <li>트리플 머쉬룸 치즈 샌드위치</li>
              <li>로스트 치킨 샐러드 밀 박스</li>
              <li>B.E.L.T 샌드위치</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>과일</span>
            <ul>
              <li>하루 한 컵 RED</li>
              <li>한라봉 가득 핸디 젤리</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>스낵</span>
            <ul>
              <li>리저브 초콜릿 세트</li>
              <li>로스티드 아몬드 앤 초콜릿</li>
              <li>마카롱</li>
              <li>자일리톨 캔디 크리스탈 민트</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>아이스크림</span>
            <ul>
              <li>자바 칩 유기농 바닐라 아이스크림</li>
              <li>넛츠 초콜릿 아포가토</li>
              <li>바닐라 아포가토</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>
        <input type="checkbox" />
        <span>굿즈</span>
        <ul>
          <li>
            <input type="checkbox" />
            <span>머그</span>
            <ul>
              <li>우리 한글 블랙 머그 473ml</li>
              <li>서울 투어 머그 355ml</li>
              <li>스타벅스 1호점 머그 400ml</li>
              <li>서울 제주 데이머그 세트</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>텀블러</span>
            <ul>
              <li>SS 부산 투어 텀블러 355ml</li>
              <li>SS 블랙 헤리티지 오드리 텀블러 355ml</li>
              <li>SS 에치드 실버 텀블러 473ml</li>
            </ul>
          </li>
          <li>
            <input type="checkbox" />
            <span>악세사리</span>
            <ul>
              <li>리저브 오렌지 카드 홀더</li>
              <li>스타벅스 1호점 에코백</li>
              <li>스타벅스 1호점 랩탑 파우치</li>
            </ul>
          </li>
        </ul>
      </li>
      <li>
        <input type="checkbox" />
        <span>카드</span>
        <ul>
          <li>10000원권</li>
          <li>30000원권</li>
          <li>50000원권</li>
          <li>100000원권</li>
        </ul>
      </li>
    </ul>
  </body>

위 HTML은 보다시피 li의 내부에 ul이 또 있고 ul이 또 있는 중첩 되는 구조입니다. 그 외에 input 체크박스도 있고 span도 존재합니다. 하위에 자식 노드가 있을 때만 ul과 input이 존재하는 걸 볼 수 있지요. 

하지만 자식 노드가 없다면 li에 각각 name만 들어오면 될 것 같네요. 

 

수월한 자료 가공을 위해 주어진 원본 데이터는 아래와 같은 모양입니다.

const menu = [
  {
    type: 'group',
    name: '음료',
    children: [
      {
        type: 'group',
        name: '콜드 브루',
        children: [
          { type: 'item', name: '나이트로 콜드 브루' },
          { type: 'item', name: '돌체 콜드 브루' },
          { type: 'item', name: '제주 비자림 콜드 브루' },
          { type: 'item', name: '콜드 브루' },
        ],
      },
      {
        type: 'group',
        name: '프라푸치노',
        children: [
          { type: 'item', name: '애플 쿠키 크림 프라푸치노' },
          { type: 'item', name: '더블 에스프레소 칩 프라푸치노' },
          { type: 'item', name: '모카 프라푸치노' },
          { type: 'item', name: '피스타치오 크림 프라푸치노' },
        ],
      },
      {
        type: 'group',
        name: '블렌디드',
        children: [
          { type: 'item', name: '망고 바나나 블렌디드' },
          { type: 'item', name: '딸기 요거트 블렌디드' },
          { type: 'item', name: '자몽 셔벗 블렌디드' },
          { type: 'item', name: '피치 & 레몬 블렌디드' },
        ],
      },
      {
        type: 'group',
        name: '티',
        children: [
          { type: 'item', name: '라임 패션 티' },
          { type: 'item', name: '민트 블렌드 티' },
          { type: 'item', name: '아이스 유스베리 티' },
          { type: 'item', name: '아이스 캐모마일 블렌드 티' },
        ],
      },
      {
        type: 'group',
        name: '주스',
        children: [
          { type: 'item', name: '한방에 쭉 감당' },
          { type: 'item', name: '파이팅 청귤' },
          { type: 'item', name: '딸기주스' },
          { type: 'item', name: '도와주 흑흑' },
        ],
      },
    ],
  },
  {
    type: 'group',
    name: '음식',
    children: [
      {
        type: 'group',
        name: '빵',
        children: [
          { type: 'item', name: '트러플 미니 스콘' },
          { type: 'item', name: '보늬밤 몽블랑 데니쉬' },
          { type: 'item', name: '고소한 치즈 베이글' },
          { type: 'item', name: '미니 클래식 스콘' },
        ],
      },
      {
        type: 'group',
        name: '케이크',
        children: [
          { type: 'item', name: '밀당 에그 타르트' },
          { type: 'item', name: '마스카포네 티라미수 케이크' },
          { type: 'item', name: '블루베리 쿠키 치즈 케이크' },
          { type: 'item', name: '부드러운 생크림 카스텔라' },
        ],
      },
      {
        type: 'group',
        name: '샌드위치',
        children: [
          { type: 'item', name: '애플 까망베르 샌드위치' },
          { type: 'item', name: '트리플 머쉬룸 치즈 샌드위치' },
          { type: 'item', name: '로스트 치킨 샐러드 밀 박스' },
          { type: 'item', name: 'B.E.L.T 샌드위치' },
        ],
      },
      {
        type: 'group',
        name: '과일',
        children: [
          { type: 'item', name: '하루 한 컵 RED' },
          { type: 'item', name: '한라봉 가득 핸디 젤리' },
        ],
      },
      {
        type: 'group',
        name: '스낵',
        children: [
          { type: 'item', name: '리저브 초콜릿 세트' },
          { type: 'item', name: '로스티드 아몬드 앤 초콜릿' },
          { type: 'item', name: '마카롱' },
          { type: 'item', name: '자일리톨 캔디 크리스탈 민트' },
        ],
      },
      {
        type: 'group',
        name: '아이스크림',
        children: [
          { type: 'item', name: '자바 칩 유기농 바닐라 아이스크림' },
          { type: 'item', name: '넛츠 초콜릿 아포가토' },
          { type: 'item', name: '바닐라 아포가토' },
        ],
      },
    ],
  },
  {
    type: 'group',
    name: '굿즈',
    children: [
      {
        type: 'group',
        name: '머그',
        children: [
          { type: 'item', name: '우리 한글 블랙 머그 473ml' },
          { type: 'item', name: '서울 투어 머그 355ml' },
          { type: 'item', name: '스타벅스 1호점 머그 400ml' },
          { type: 'item', name: '서울 제주 데이머그 세트' },
        ],
      },
      {
        type: 'group',
        name: '텀블러',
        children: [
          { type: 'item', name: 'SS 부산 투어 텀블러 355ml' },
          { type: 'item', name: 'SS 블랙 헤리티지 오드리 텀블러 355ml' },
          { type: 'item', name: 'SS 에치드 실버 텀블러 473ml' },
        ],
      },
      {
        type: 'group',
        name: '악세사리',
        children: [
          { type: 'item', name: '리저브 오렌지 카드 홀더' },
          { type: 'item', name: '스타벅스 1호점 에코백' },
          { type: 'item', name: '스타벅스 1호점 랩탑 파우치' },
        ],
      },
    ],
  },
  {
    type: 'group',
    name: '카드',
    children: [
      { type: 'item', name: '10000원권' },
      { type: 'item', name: '30000원권' },
      { type: 'item', name: '50000원권' },
      { type: 'item', name: '100000원권' },
    ],
  },
];

이제 위와 같이 주어진 자료를 Tree UI로 만들어주어야 합니다. 반드시 재귀함수를 사용해야 하니, 스스로를  불러올 수 있도록 해야겠네요. 배열 내부의 배열을 조회하는 과정에서 재귀 함수를 사용하면 될 것 같습니다.(유사 구조 반복)

 

createTreeView 라는 지정된 함수 내부에서 이제 Tree UI를 만들어줄 겁니다.

// TODO: createTreeView 함수를 재귀(자기 자신을 계속 부르게 함)호출하여 테스트케이스를 통과하세요.
// GOAL: 최종 결과가 resut.html와 같은 모습으로 나와야 합니다.


//재귀를 사용해서 html을 만든다.
//root로 불러온 것이 currentNode로 들어간다. 
const root = document.getElementById('root');
function createTreeView(menu, currentNode) {
  //메뉴가 들어올 거임. 메뉴에는 4가지 그룹이 존재한다.
  for(let i=0;i<menu.length;i++){
    //메뉴가 가진 list 수만큼 li를 구축한다.
    //createElement로 li를 만든다.
  const menuLi = document.createElement('li');
  //children, 즉 하위 항목이 존재할 때로 나눈다. 
  if(menu[i].children){
    //하위 자식노드가 존재한다면, 
    //자식노드 아래 새 ul이 존재해야 한다. 
  const input = document.createElement('input');
  input.type= 'checkbox';
  const span = document.createElement('span');
  span.textContent = menu[i].name;
  const ul = document.createElement('ul');
  menuLi.append(input, span, ul);  
  currentNode.append(menuLi);
  
  createTreeView(menu[i].children, ul)
  //여기서 재귀함수를 쓴다. 
  } 
  else {
    //자식노드가 존재하지 않는다면, name만 표시한다!
    menuLi.textContent = menu[i].name;
    currentNode.append(menuLi)
  }
  }
}

createTreeView(menu, root);

    

    ✓ ul#root 엘리먼트 안에 카테고리(음료, 음식, 굿즈, 카드)를 렌더링 할 4개의 li 엘리먼트가 있어야 합니다
    ✓ 카테고리(음료, 음식, 굿즈, 카드) 엘리먼트 안에는 각각 자식 노드를 보여주고 감춰줄 checkbox가 존재해야 합니다
    ✓ 음료, 음식, 굿즈, 카드 카테고리 이름(name)을 span 태그로 감싸야 합니다
    ✓ 자식 노드가 없는 데이터의 경우, li 엘리먼트 안에 단순히 이름(name)만 표시합니다. (checkbox 및 span, ul을 포함하면 안됩니다)
    ✓ 자식 노드가 있는 데이터의 경우, li 엘리먼트 아래에 자식 노드를 렌더링할 새로운 ul이 존재해야 합니다 (1)
    ✓ 자식 노드가 있는 데이터의 경우, li 엘리먼트 아래에 자식 노드를 렌더링할 새로운 ul이 존재해야 합니다 (2)
    ✓ type 속성이 item인 객체는 li 태그로 name 속성값을 감싸주어야 합니다

 

 

만족시켜야 하는 조건은 위와 같네요. 이를 가볍게 다시 제 수도코드로 적어보면 이렇게 됩니다. 

 

1) root 내부에서 for문을 써서 li 가 존재하는 만큼 새로 생성한다.

2) createElement와 append를 적절하게 사용하며, 만든 요소를 append 해준다.

3) 자식 노드가 존재하는 경우 / 없는 경우로 조건if을 나누어서 생각한다. (menu[i].children은 i 번째 메뉴에 자식 노드가 존재함을 의미한다.)자식이 존재할 때는 input, span, ul을 넣어주고, 존재하지 않으면 li.textContent로 이름을 넣어준다. 

4) createTreeView(menu[i].children, ul) 로 자식 노드 내부에서 재귀를 행해준다.

 

이처럼 내부의 요소들까지 계속 조회하면서 여러 번 똑같은 구조를 만들어줄 때는,

자기 자신을 다시 부르는 재귀함수가 유용하게 쓰일 것 같습니다. 

'CODE STATES 44' 카테고리의 다른 글

31~35일차 스터디  (0) 2023.04.16
UX/UI로 사용자 친화 웹 제작  (0) 2023.04.13
Section 2 회고  (1) 2023.04.10
26일차~30일차 스터디  (0) 2023.03.30
REST API, Open API  (0) 2023.03.29
Comments