๋ฐ˜์‘ํ˜•

๋ชฉํ‘œ


์•„๋ž˜ 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;
}
}

 


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

๋Œ“๊ธ€

๋Œ“๊ธ€์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.