목표


아래 DOM 구조에서 가장 안쪽 요소부터 시작해 부모 요소로 갈 수록 중첩 레벨이 1씩 늘어나고, class에 대응하는 dataset에 중첩 레벨 값을 할당해야 한다. 예를들어 class가 "clause" 이고, 해당 요소의 중첩 레벨이 2라면 data-clause-lv="2" 속성을 할당한다.

<p ref={ref}>
  <span class="clause" data-clause-lv="2">
    <span class="word" data-word-lv="1">
			<span data-index="0">Hello</span>
		</span>
  </span>
</p>

만약 자식 요소가 2개 이상일 땐 자식 요소들중 중첩 레벨이 가장 높은 값 + 1이 부모 요소의 중첩 레벨이 된다. 아래 예시를 기준으로 1번째 자식의 중첩 레벨(data-word-lv="1") 보다, 2번째 자식의 중첩 레벨(data-phrase-lv="2")이 더 높으므로, 부모 요소의 중첩레벨은 3이 된다(data-clause-lv="3").

<p ref={ref}>
  <span class="clause" data-clause-lv="3">  <!-- 부모 -->
    <span class="word" data-word-lv="1"> <!-- 자식 1 -->
			<span data-index="0">Hello</span>
		</span>
    <span class="phrase" data-phrase-lv="2"> <!-- 자식 2 -->
			<span class="word" data-word-lv="1">
				<span data-index="1">World</span>
			</span>
		</span>
  </span>
</p>

예제


0~13은 토큰 인덱스

0~13은 토큰 인덱스

영어 문장에서 단어 혹은 문장부호를 인덱스 기준으로 잡고(토큰 인덱스), 그 토큰들이 서로 어떻게 연결되어 문장을 구성하는지에 대한 정보를 중첩 구조로 표현하면 아래와 같은 DOM 구조를 갖는다. 이제 아래 DOM 구조에서 ?로 표시된 중첩 깊이를 계산해야 한다.

<p class="text-xl">
  <span data-index="0">I</span>
  <span data-index="1">am</span>
  <span class="kc phrase" data-phrase-lv="?">
    <span data-index="2">a</span>
    <span data-index="3">boy</span>
    <span class="kc clause" data-clause-lv="?">
      <span data-index="4">who</span>
      <span data-index="5">likes</span>
      <span class="kc phrase" data-phrase-lv="?">
        <span class="kc phrase" data-phrase-lv="?">
          <span data-index="6">to</span>
          <span class="kc word" data-word-lv="?">
            <span class="kc word" data-word-lv="?">
              <span class="kc word" data-word-lv="?">
                <span data-index="7">play</span>
              </span>
            </span>
          </span>
					<span data-index="8">tennis</span>
        </span>
        <span class="kc clause" data-clause-lv="?">
          <span data-index="9">which</span>
          <span data-index="10">is</span>
          <span class="kc word" data-word-lv="?">
            <span class="kc word" data-word-lv="?">
              <span data-index="11">fun</span>
            </span>
          </span>
          <span data-index="12">.</span>
        </span>
      </span>
    </span>
  </span>
</p>

구현



DFS 노드 방문 순서(검정 배경 숫자) 및 각 노드 반환값(점선 화살표)

DFS 노드 방문 순서(검정 배경 숫자) 및 각 노드 반환값(점선 화살표)

assignCalculatedLevel 함수는 자식 요소 중 가장 높은 중첩 레벨을 찾는 것을 목표로 한다. 이를 위해 자식 요소가 있을 때마다 자신을 재귀적으로 호출하는 DFS 방식으로 탐색하여 가장 안쪽 요소부터 계산해 나간다.

이 과정에서 word, phrase, clause 클래스를 가진 요소에 대해선 해당 중첩 레벨 정보를 data- 속성에 할당한다. 이렇게 각 요소의 최대 중첩 레벨이 결정되면 그 값을 반환하여 상위 요소의 중첩 레벨 계산에 활용한다.

const assignCalculatedLevel = (element: HTMLElement) => {
	// element 자식 요소중 가장 높은 레벨을 저장할 변수
  let maxChildLevel = 0;

	// element의 모든 자식 요소 순회
  for (const child of element.children) {
		// 깊이 우선 탐색 방식으로 자식 요소 레벨 계산
    const childLevel = assignCalculatedLevel(child as HTMLElement);
		// maxChildLevel, childLevel 둘 중 더 큰 값을 maxChildLevel로 지정
    maxChildLevel = Math.max(maxChildLevel, childLevel);
  }
	
  const hasChild = element.children.length > 0;
	// 현재 element에 자식 요소가 있으면 maxChildLevel + 1 값을 현재 요소의 레벨로 지정
	// 현재 element에 자식 요소가 없으면 maxChildLevel 값을 현재 요소의 레벨로 지정
  const currentLevel = hasChild ? maxChildLevel + 1 : maxChildLevel;

  const classesToCheck: TagType[] = ['word', 'phrase', 'clause'];

  classesToCheck.forEach((className) => {
		// 현재 요소가 classesToCheck 배열에 있는 클래스를 포함하는지 검사
    if (element.classList.contains(className)) {
			// 클래스를 포함하면 해당 클래스에 대한 레벨 정보를 `data-${className}Lv` 속성에 할당
      element.dataset[`${className}Lv`] = `${currentLevel}`;
    }
  });

	// 현재 요소의 레벨 값 반환
  return currentLevel;
};

const calculateNestingLevel = (ref: RefObject<HTMLParagraphElement>) => {
  const spans = ref.current?.children;
  if (!spans) return;

  Array.from(spans).forEach((span) => {
    const spanElement = span as HTMLElement;
		// dataset.kc가 있는 요소에 대해서만 호출
    if (spanElement.dataset.kc) assignCalculatedLevel(spanElement);
  });
};