๋ฐ˜์‘ํ˜•

๋ชฉํ‘œ


์•„๋ž˜ 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์€ ํ† ํฐ ์ธ๋ฑ์Šค

์˜์–ด ๋ฌธ์žฅ์—์„œ ๋‹จ์–ด ํ˜น์€ ๋ฌธ์žฅ๋ถ€ํ˜ธ๋ฅผ ์ธ๋ฑ์Šค ๊ธฐ์ค€์œผ๋กœ ์žก๊ณ (ํ† ํฐ ์ธ๋ฑ์Šค), ๊ทธ ํ† ํฐ๋“ค์ด ์„œ๋กœ ์–ด๋–ป๊ฒŒ ์—ฐ๊ฒฐ๋˜์–ด ๋ฌธ์žฅ์„ ๊ตฌ์„ฑํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ค‘์ฒฉ ๊ตฌ์กฐ๋กœ ํ‘œํ˜„ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ 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 ๋…ธ๋“œ ๋ฐฉ๋ฌธ ์ˆœ์„œ(๊ฒ€์ • ๋ฐฐ๊ฒฝ ์ˆซ์ž) ๋ฐ ๊ฐ ๋…ธ๋“œ ๋ฐ˜ํ™˜๊ฐ’(์ ์„  ํ™”์‚ดํ‘œ)

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);
  });
};
๋”๋ณด๊ธฐ

๊ฐ ์š”์†Œ์— ์ฃผ์„์œผ๋กœ ์ถ”๊ฐ€ํ•œ ์ˆซ์ž ์†Œ๊ด„ํ˜ธ๋Š” ๊ฐ€์žฅ ์•ˆ์ชฝ ์š”์†Œ๊นŒ์ง€ ๋ฐฉ๋ฌธํ•œ ๋’ค ๋ฆฌํ„ดํ•˜๋Š” ์ˆœ์„œ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค

<p class="text-xl"> <!-- calculateNestingLevel ํ•จ์ˆ˜ ์‹œ์ž‘ -->
โ”‚
โ”œโ”€โ”€ <span data-index="0">I</span> <!-- skip -->
โ”œโ”€โ”€ <span data-index="1">am</span> <!-- skip -->
โ””โ”€โ”€ <span class="kc phrase" data-phrase-lv="7"> <!-- assignCalculatedLevel ํ•จ์ˆ˜ ์‹œ์ž‘ -->
    โ”œโ”€โ”€ <span data-index="2">a</span> <!-- return 0 -->
    โ”œโ”€โ”€ <span data-index="3">boy</span> <!-- return 0 -->
    โ””โ”€โ”€ <span class="kc clause" data-clause-lv="6"> <!-- (13) return 6 -->  
        โ”œโ”€โ”€ <span data-index="4">who</span> <!-- return 0 -->
        โ”œโ”€โ”€ <span data-index="5">likes</span> <!-- return 0 -->
        โ””โ”€โ”€ <span class="kc phrase" data-phrase-lv="5"> <!-- (12) return 5 -->  
            โ”œโ”€โ”€ <span class="kc phrase" data-phrase-lv="4"> <!-- (6) return 4 -->
            โ”‚   โ”œโ”€โ”€ <span data-index="6">to</span> <!-- return 0 -->
            โ”‚   โ”œโ”€โ”€ <span class="kc word" data-word-lv="3"> <!-- (4) return 3 -->
            โ”‚   โ”‚   โ””โ”€โ”€ <span class="kc word" data-word-lv="2"> <!-- (3) return 2 -->
            โ”‚   โ”‚       โ””โ”€โ”€ <span class="kc word" data-word-lv="1"> <!-- (2) return 1 -->
            โ”‚   โ”‚           โ””โ”€โ”€ <span data-index="7">play</span> <!-- (1) return 0 -->
            โ”‚   โ””โ”€โ”€ <span data-index="8">tennis</span> <!-- (5) return 0 -->
            โ””โ”€โ”€ <span class="kc clause" data-clause-lv="3"> <!-- (11) return 3 -->  
                โ”œโ”€โ”€ <span data-index="9">which</span> <!-- return 0 -->
                โ”œโ”€โ”€ <span data-index="10">is</span> <!-- return 0 -->
                โ”œโ”€โ”€ <span class="kc word" data-word-lv="2"> <!-- (9) return 2 --> 
                โ”‚    โ””โ”€โ”€ <span class="kc word" data-word-lv="1"> <!-- (8) return 1 --> 
                โ”‚        โ””โ”€โ”€ <span data-index="11">fun</span> <!-- (7) return 0 -->
                โ””โ”€โ”€ <span data-index="12">.</span> <!-- (10) return 0 -->

 

๋ฒˆ์™ธ


word, phrase, clause ์ค‘์ฒฉ ๋ ˆ๋ฒจ ์ •๋ณด๋ฅผ ํ•ด๋‹นํ•˜๋Š” ์š”์†Œ์˜ data-* ์†์„ฑ์— ํ• ๋‹นํ–ˆ์œผ๋ฏ€๋กœ ๋ ˆ๋ฒจ ๊ฐ’์— ๋Œ€์‘ํ•˜๋Š” ๋†’์ด๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ๋“ค์–ด ๊ธฐ๋ณธ ๋†’์ด๊ฐ€ 1rem์ด๋ฉด ๋ ˆ๋ฒจ 1๋ถ€ํ„ฐ 1rem * 1, 1rem * 2,, ... ๋ฐฉ์‹์œผ๋กœ ๋†’์ด๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ.

 

SCSS์˜ @for๋ฌธ์„ ์ด์šฉํ•˜๋ฉด ๊ฐ ๋ ˆ๋ฒจ์— ๋Œ€ํ•œ ์Šคํƒ€์ผ์„ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

$clause-padding-base: 0.1rem; // ์ ˆ/๊ตฌ ํ•˜๋‹จ ์—ฌ๋ฐฑ
$clause-top-base: 1.5rem; // ์ ˆ/๊ตฌ ์ด๋ฆ„ ์ƒ๋‹จ ์—ฌ๋ฐฑ
$clause-offset-base: 1.3rem; // ์ ˆ/๊ตฌ ์ ์„  ์ƒํ•˜ ๊ฐ„๊ฒฉ
$word-top-base: 1.5rem; // ๋‹จ์–ด ์ƒ๋‹จ ์—ฌ๋ฐฑ
$word-offset-base: 1rem; // ๋‹จ์–ด ์ƒํ•˜ ๊ฐ„๊ฒฉ

@for $i from 1 through 30 {
  $offset: $clause-offset-base * ($i - 1);

  .kc.clause[data-clause-lv='#{$i}'],
  .kc.phrase[data-phrase-lv='#{$i}'] {
    padding-bottom: $clause-padding-base + $offset;

    &:after {
      top: $clause-padding-base + $clause-top-base + $offset;
    }
  }
}

@for $i from 1 through 30 {
  $offset: $word-offset-base * ($i - 1);

  .kc.word[data-word-lv='#{$i}']:after {
    top: $word-top-base + $offset;
  }
}

 


๊ธ€ ์ˆ˜์ •์‚ฌํ•ญ์€ ๋…ธ์…˜ ํŽ˜์ด์ง€์— ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ๋ฐ˜์˜๋ฉ๋‹ˆ๋‹ค. ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”
๋ฐ˜์‘ํ˜•