๐จ Tailwind Advanced 3์ฅ: ํ์ดํฌ๊ทธ๋ํผ ์ฌํ โ ๊ฐ๋ ์ฑ๊ณผ ์๊ฐ ์๊ณ์ ๊ธฐ์
๐ ๊ฐ์
line-clamp, text-balance, font-variant-numeric, @tailwindcss/typography ํ๋ฌ๊ทธ์ธ, fluid typography๊น์ง โ ํ ์คํธ๋ฅผ ์์ ํ ์ ์ดํฉ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- โ๏ธ 1๋จ๊ณ: line-clamp โ ์ค ์ ๊ณ ์ ์ ๊ธฐ์
- โ๏ธ 2๋จ๊ณ: text-balance & text-pretty โ ํ ์คํธ ๊ท ํ์ ๋ฏธํ
- ๐ข 3๋จ๊ณ: font-variant-numeric โ ์ซ์๋ฅผ ๋์์ธํ๋ค
- ๐ฌ 4๋จ๊ณ: font-feature-settings โ OpenType ๊ธฐ๋ฅ ์ ๊ธ ํด์
- ๐ 5๋จ๊ณ: @tailwindcss/typography โ Prose ํ๋ฌ๊ทธ์ธ ๋ง์คํฐ
- ๐ 6๋จ๊ณ: Fluid Typography โ ๋ทฐํฌํธ์ ๋ฐ์ํ๋ ๊ธ์ ํฌ๊ธฐ
- ๐ ์ค์ : ์์๋ค ์ปค๋ฎค๋ํฐ ์คํฐ๋ ์นด๋ + ๊ฒ์๊ธ ์์ธ ํ์ด์ง
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 28๋ถ (์ ์ฒด) / ํต์ฌ ํํธ๋ง: 14๋ถ
๐บ๏ธ ์ด ๋ฌธ์์ ๋ฐฐ๊ฒฝ ์ธ๊ณ๊ด: '์์๋ค ์ปค๋ฎค๋ํฐ'
- ์์(๋์์ด๋): "์์ฒ ๋! ์คํฐ๋ ์นด๋ ๋์์ธ ํ์ธํด๋ดค๋๋ฐ์, ์นด๋ ํ์ดํ์ด ์งง์ ๊ฑด 1์ค, ๊ธด ๊ฑด 3์ค๊น์ง ๋์ด๋์ ์นด๋ ๋์ด๊ฐ ์ ๊ฐ๊ฐ์ด์์. ์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์์ด ์๋ง์ด ๋๊ณ ์์ด์. 2์ค๋ก ๊ณ ์ ํด ์ฃผ์ธ์!"
- ์์ฒ (์ ์
): "์, ๊ทธ๊ฑฐ์. ์ ๊ฐ JavaScript๋ก ๋ฌธ์์ด ๊ธธ์ด ์ฒดํฌํด์ ์ผ์ ๊ธธ์ด ๋์ผ๋ฉด
...์ผ๋ก ์๋ฅด๋ ํจ์ ๋ง๋ค๋ฉด ๋๊ฒ ๋๋ฐ์? ์๋๋ฉด CSSwhite-space: nowrap; overflow: hidden; text-overflow: ellipsis;์ด๋ ๊ฒ ํ๋ฉด 1์ค ์๋ฅด๊ธฐ๋ ๋๋๋ฐ, 2์ค์..." - ์ํธ(๋ฆฌ๋): "์์ฒ ๋.
line-clamp-2ํ๋๋ฉด ๋ฉ๋๋ค. ์ฌ์ง์ด...๋ง์ค์ํ๋ ์๋์ผ๋ก ๋ถ์ด์."
๐ฏ ์ด ๋ฌธ์๋ฅผ ๋ค ์ฝ์ผ๋ฉด ํ ์ ์๋ ๊ฒ
-
line-clamp-*์ ๋ด๋ถ ๊ตฌํ ์๋ฆฌ๋ฅผ ์ดํดํ๊ณ ,truncate์์ ์ฐจ์ด๋ฅผ ์ค๋ช ํ ์ ์๋ค. -
text-balance,text-pretty๋ก ์ ๋ชฉ๊ณผ ๋ณธ๋ฌธ์ ์ค๋ฐ๊ฟ ํ์ง์ ๊ฐ์ ํ ์ ์๋ค. -
tabular-nums,slashed-zero๋ฑ์ผ๋ก ๋ฐ์ดํฐ ํ ์ด๋ธ์ ์ซ์ ์ ๋ ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์๋ค. -
proseํ๋ฌ๊ทธ์ธ์ผ๋ก ๋งํฌ๋ค์ด ๋ ๋๋ง ์์ญ์ ์ฆ์ ์คํ์ผ๋งํ ์ ์๋ค. -
clamp()ํจ์๋ก Fluid Typography๋ฅผ ๊ตฌํํ๋ ์๋ฆฌ๋ฅผ ์ดํดํ๋ค.
๐บ๏ธ ์ด ๋ฌธ์์ ํ๋ฆ
์์์ด์ ์นด๋ ๋ ์ด์์ ๋ฌธ์ โ line-clamp ๋ง์คํฐ โ text-balance โ ์ซ์ ํ์ดํฌ๊ทธ๋ํผ โ prose ํ๋ฌ๊ทธ์ธ โ fluid typography โ ์ค์ ๊ตฌํ
๐ค ์ ์์์ผ ํ๋๊ฐ
ํ์ดํฌ๊ทธ๋ํผ๋ UI์์ ๊ฐ์ฅ ๊ณผ์ํ๊ฐ๋๋ ๋ถ์ผ์ผ. ๋๋ถ๋ถ์ ๊ฐ๋ฐ์๋ font-size์ font-weight ์ ๋๋ก ์ถฉ๋ถํ๋ค๊ณ ์๊ฐํ๊ฑฐ๋ .
ํ์ง๋ง ์ค๋ฌด์์ ๋์์ด๋์ ํจ๊ป ์ผํด๋ณธ ๊ฒฝํ์ด ์๋ค๋ฉด, ์ด๋ฐ QA ์ด์๋ค์ ํ ๋ฒ์ฏค ๋ง์ฃผ์ณค์ ๊ฑฐ์ผ:
- ์นด๋ ์ ๋ชฉ ๊ธธ์ด๊ฐ ๋ฌ๋ผ์ ๊ทธ๋ฆฌ๋ ์ ๋ ฌ์ด ๋ฌด๋์ง
- ํฐ ํ๋ฉด์์ h1 ์ ๋ชฉ์ด ๋ง์ง๋ง ๋จ์ด ํ๋๋ง ํผ์ 2๋ฒ์งธ ์ค์ ๋๋ ์์
- ๋์๋ณด๋์ ์ซ์ ์ปฌ๋ผ์์ ์ซ์ ํญ์ด ๋ฌ๋ผ์ ์์์ ์ด ์ ๋ง์
- ๋ธ๋ก๊ทธ ๋งํฌ๋ค์ด ์ฝํ ์ธ ์ ์คํ์ผ์ด ์ ํ ์์ด์ ๋ ๊ฒ์ HTML๋ง ๋ณด์
์ด ๋ชจ๋ ๊ฒ๋ค์ด CSS ํ์ดํฌ๊ทธ๋ํผ ์์ฑ ํ๋ํ๋๋ก ํด๊ฒฐ๋ผ. ๊ทธ๋ฆฌ๊ณ Tailwind๋ ์ด๋ฅผ ์ง๊ด์ ์ธ ์ ํธ๋ฆฌํฐ๋ก ์ถ์ํํด๋จ์ด.
๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
๐ฐ ์ ๋ฌธ ํธ์ง์์ ๋ ์ด์์ ์์
์ ๋ฌธ์ฌ ํธ์ง์๋ ๊ธฐ์ฌ๋ฅผ ๋ฐ์ผ๋ฉด ํ์ด์ง์ ์ ํํ ๋ง๋๋ก ์๋ฅด๊ณ ์ ๋ ฌํด์ผ ํด.
line-clamp: ๊ธฐ์ฌ๊ฐ ๋๋ฌด ๊ธธ๋ฉด ์ ํด์ง ์ค ์์์ "..." ์ผ๋ก ์๋ฅด๋ ๊ธฐ์ . ํธ์ง์๊ฐ "3๋จ๋ฝ๊น์ง๋ง 1๋ฉด์ ๋ฃ์ด" ํ๋ ๊ฒ.text-balance: ์ ๋ชฉ์ด ํ ์ค์ ๋ฑ ๋ง์ง ์์ ๋ ์ค๋ฐ๊ฟ์ ๊ท ํ ์๊ฒ ๋๋๋ ๊ธฐ์ . "์ ๋ชฉ์ด ๋์์ ๋จ์ด ํ๋๋ง ๋จ์ด์ง์ง ์๊ฒ ํด์ค."tabular-nums: ์ฌ๋ฌด ๋ณด๋์์ ๊ธ์ก ์ปฌ๋ผ์ ์ซ์ ํญ์ ์ผ์ ํ๊ฒ ๋ง์ถฐ์ ์์์ ์ด ์ธ๋ก๋ก ์ ๋ ฌ๋๊ฒ ํ๋ ๊ธฐ์ .prose: ๊ธฐ์ฌ ๋ณธ๋ฌธ์ ์๋์ผ๋ก ์ ๋ฌธ ์คํ์ผ(์ ๋ชฉ ํฌ๊ธฐ, ๋ฌธ๋จ ๊ฐ๊ฒฉ, ์ธ์ฉ๊ตฌ ์คํ์ผ)์ ์ ํ๋ ์คํ์ผ ์ํธ.
โ๏ธ 1๋จ๊ณ: line-clamp โ ์ค ์ ๊ณ ์ ์ ๊ธฐ์
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
line-clamp-*์ ํธ๋ฆฌํฐ๊ฐ ๋ด๋ถ์ ์ผ๋ก ์ด๋ค CSS๋ฅผ ์์ฑํ๋์ง ์ดํดํ๋ค.truncate(1์ค ์๋ฅด๊ธฐ)์line-clamp-*(N์ค ์๋ฅด๊ธฐ)์ ์ฐจ์ด๋ฅผ ๋ช ํํ ๊ตฌ๋ถํ๋ค.
truncate ์ line-clamp ์ ์ฐจ์ด
๋ ์ ํธ๋ฆฌํฐ๋ "๋์น๋ ํ ์คํธ๋ฅผ ์๋ฅธ๋ค"๋ ๋ชฉ์ ์ ๊ฐ์ง๋ง, ์๋ ๋ฐฉ์์ด ์์ ํ ๋ฌ๋ผ.
<!-- truncate: 1์ค ๊ฐ์ + overflow ์จ๊น + ... ํ์ -->
<!-- ์ฌ์ฉํ๋ CSS:
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; โ ์ด๊ฒ ํต์ฌ! ์ค๋ฐ๊ฟ์ ์์ ํ ๋ง์
-->
<p class="truncate w-64">
์ด ํ
์คํธ๋ ๋ฌด์กฐ๊ฑด 1์ค๋ก๋ง ํ์๋๊ณ ๋๋จธ์ง๋ ...์ผ๋ก ์๋ ค
</p>
<!-- line-clamp-N: N์ค๊น์ง๋ง ํ์ฉ + overflow ์จ๊น + ... ํ์ -->
<!-- ์ฌ์ฉํ๋ CSS:
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: N; โ N์ค๊น์ง ํ์ฉ!
-->
<p class="line-clamp-2 w-64">
์ด ํ
์คํธ๋ 2์ค๊น์ง๋ ๋ณด์ฌ์ค.
3๋ฒ์งธ ์ค๋ถํฐ๋ ์จ๊ธฐ๊ณ ...์ผ๋ก ํ์ํด.
๊ธด ํ
์คํธ๊ฐ ๊ณ์๋์ด๋ 2์ค์์ ๋ฉ์ถฐ.
</p>์ค์ : ์นด๋ ๊ทธ๋ฆฌ๋ ๋ ์ด์์ ๊ณ ์
// components/StudyCard.tsx
// โ ์์ฒ ์ด์ ์ด๊ธฐ ์ฝ๋: ์ ๋ชฉ ๊ธธ์ด์ ๋ฐ๋ผ ์นด๋ ๋์ด๊ฐ ๋ฌ๋ผ์ง
export function NaiveStudyCard({ title, description }) {
return (
<div className="bg-white rounded-xl p-4 shadow">
{/* ๐ฃ ์์ฒ : ์ ๋ชฉ์ด 1์ค์ผ ๋๋ ์นด๋๊ฐ ์๊ณ , 3์ค์ผ ๋๋ ์ปค์ ๊ทธ๋ฆฌ๋๊ฐ ๊นจ์ง */}
<h3 className="font-semibold text-gray-900">{title}</h3>
<p className="text-gray-600 text-sm mt-2">{description}</p>
</div>
);
}
// โ
์ํธ์ ๋ฆฌํฉํ ๋ง: line-clamp์ผ๋ก ์นด๋ ๋์ด ๊ณ ์
export function StudyCard({ title, description, memberCount, topic }) {
return (
<div className="bg-white rounded-xl p-4 shadow flex flex-col">
{/* ํ ํฝ ํ๊ทธ */}
<span className="text-xs font-medium text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-full w-fit">
{topic}
</span>
{/* ์ ๋ชฉ: ์ต๋ 2์ค, ๋์ผ๋ฉด ... */}
<h3 className="
font-semibold text-gray-900 mt-2
line-clamp-2 /* 2์ค ์ดํ ๋ง์ค์ํ */
/* ๋ด๋ถ์ ์ผ๋ก: display: -webkit-box + -webkit-line-clamp: 2 */
">
{title}
</h3>
{/* ์ค๋ช
: ์ต๋ 3์ค */}
<p className="text-gray-600 text-sm mt-2 line-clamp-3 flex-1">
{description}
</p>
{/* ํ๋จ ๋ฉํ ์ ๋ณด (ํญ์ ์นด๋ ํ๋จ์ ๊ณ ์ ) */}
<div className="mt-4 pt-3 border-t border-gray-100 flex justify-between items-center">
<span className="text-sm text-gray-500">๋ฉค๋ฒ {memberCount}๋ช
</span>
<button className="text-sm font-medium text-indigo-600 hover:text-indigo-700">
์ฐธ์ฌํ๊ธฐ โ
</button>
</div>
</div>
);
}line-clamp ํด์ : ๋ฐ์ํ ์ ์ฉ
<!-- ๋ชจ๋ฐ์ผ์์๋ 3์ค, ๋ฐ์คํฌํฑ์์๋ ์ ํ ์์ -->
<p class="line-clamp-3 lg:line-clamp-none">
๋ชจ๋ฐ์ผ์์๋ 3์ค๋ง ๋ณด์ด์ง๋ง, lg ์ด์์์๋ ์ ์ฒด ํ
์คํธ๊ฐ ๋ณด์ฌ.
</p>
<!-- ์ํ ๊ธฐ๋ฐ ํ ๊ธ (React) -->
export function ExpandableText({ text }: { text: string }) {
const [expanded, setExpanded] = useState(false);
return (
<div>
<p className={expanded ? 'line-clamp-none' : 'line-clamp-3'}>
{text}
</p>
<button onClick={() => setExpanded(!expanded)} className="text-indigo-600 text-sm mt-1">
{expanded ? '์ ๊ธฐ' : '๋ ๋ณด๊ธฐ'}
</button>
</div>
);
}line-clamp ๋ฅผ ์ปค์คํ ๊ฐ์ผ๋ก
<!-- ๊ธฐ๋ณธ ์ ๊ณต: line-clamp-1 ~ line-clamp-6 -->
<p class="line-clamp-1">...</p> <!-- 1์ค -->
<p class="line-clamp-2">...</p> <!-- 2์ค -->
<p class="line-clamp-6">...</p> <!-- 6์ค -->
<!-- ์ปค์คํ
๊ฐ -->
<p class="line-clamp-[10]">10์ค๊น์ง</p>
<p class="line-clamp-[--my-line-count]">CSS ๋ณ์๋ก ์ ์ด</p>โ๏ธ 2๋จ๊ณ: text-balance & text-pretty โ ํ ์คํธ ๊ท ํ์ ๋ฏธํ
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ์ ๋ชฉ๊ณผ ๋ณธ๋ฌธ์ ๊ฐ๊ฐ ์ด๋ค text-wrap ์ ํธ๋ฆฌํฐ๊ฐ ์ ํฉํ์ง ์ ํํ ์ ์๋ค.
- ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ค๋ฐ๊ฟ์ ์ต์ ํํ๋ "๊ณ ์ ๋จ์ด(orphan)" ๋ฌธ์ ๋ฅผ ์ดํดํ๋ค.
์ด ์น์ ์ ๊ฐ๋ฐ์๋ค์ด ๊ฐ์ฅ ๋ชจ๋ฅด๋ ์จ๊ฒจ์ง ๋ณด์ ๊ฐ์ ๊ธฐ๋ฅ์ด์ผ.
๊ณ ์ ๋จ์ด(Orphan) ๋ฌธ์
์ ๋ชฉ์ด ์ด๋ ๊ฒ ํ์๋ ๋:
"๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ ํ๋ซํผ โ ํจ๊ป ์ฑ์ฅํ๋
์ปค๋ฎค๋ํฐ"
๋ง์ง๋ง์ "์ปค๋ฎค๋ํฐ" ๋จ์ด ํ๋๋ง ๋ฉ๊ทธ๋ฌ๋ 2๋ฒ์งธ ์ค์ ๋จ์ด์ ธ ์์ด.
์ด๊ฒ ๋ฐ๋ก "orphan(๊ณ ์ ๋จ์ด)" ๋ฌธ์ ์ผ. ํ์ดํฌ๊ทธ๋ํผ์์ ๊ฐ์ฅ ๋์ ๊ฑฐ์ฌ๋ฆฌ๋ ํ์ ์ค ํ๋.
text-balance โ ์ค๋ง๋ค ๋น์ทํ ๊ธธ์ด๋ก ๊ท ํ ๋ง์ถ๊ธฐ
<!-- โ ๊ธฐ๋ณธ ๋์: ์ฒซ ์ค์ ์ต๋ํ ์ฑ์ฐ๊ณ ๋๋จธ์ง๊ฐ 2๋ฒ์งธ ์ค๋ก -->
<h1 class="text-4xl font-bold max-w-sm">
๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ ํ๋ซํผ โ ํจ๊ป ์ฑ์ฅํ๋ ์ปค๋ฎค๋ํฐ
<!-- ๋ ๋๋ง:
"๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ ํ๋ซํผ โ ํจ๊ป ์ฑ์ฅํ๋" โ ๊ฝ ์ฐธ
"์ปค๋ฎค๋ํฐ" โ ๊ณ ์ ๋จ์ด! -->
</h1>
<!-- โ
text-balance: ๊ฐ ์ค์ ๊ธธ์ด๋ฅผ ์ต๋ํ ๊ท ๋ฑํ๊ฒ -->
<h1 class="text-4xl font-bold max-w-sm text-balance">
๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ ํ๋ซํผ โ ํจ๊ป ์ฑ์ฅํ๋ ์ปค๋ฎค๋ํฐ
<!-- ๋ ๋๋ง:
"๊ฐ๋ฐ์ ์คํฐ๋ ๋งค์นญ" โ ๊ท ๋ฑ
"ํ๋ซํผ โ ํจ๊ป ์ฑ์ฅํ๋ ์ปค๋ฎค๋ํฐ" โ ๊ท ๋ฑ -->
</h1>text-balance ๋ฅผ ์ธ ๋ ์ฃผ์:
- ์งง์ ์ ๋ชฉ(ํค๋ฉ)์์ ๋น์ ๋ฐํจ: 2~5์ค ์ ๋์ h1, h2 ์ ๋ชฉ์ ์ด์์
- ๊ธด ๋ณธ๋ฌธ์๋ ์ฌ์ฉํ์ง ๋ง ๊ฒ: ๋ธ๋ผ์ฐ์ ๊ฐ ๋ชจ๋ ์ค์ ๊ท ํ์ ๊ณ์ฐํ๋ ๋น์ฉ์ด ๋ฐ์
<!-- ์ฌ๋ฐ๋ฅธ ์ฌ์ฉ ํจํด -->
<h1 class="text-balance">์งง์ ํ์ด๋ก ์ ๋ชฉ</h1> <!-- โ
-->
<h2 class="text-balance">์นด๋ ์น์
์ ๋ชฉ</h2> <!-- โ
-->
<p class="text-balance">๊ธด ๋ณธ๋ฌธ ํ
์คํธ...</p> <!-- โ ์ฑ๋ฅ ์ด์ -->text-pretty โ ๋ง์ง๋ง ์ค ๊ณ ์ ๋จ์ด๋ง ๋ฐฉ์ง
text-balance๊ฐ ๋ชจ๋ ์ค์ ๊ท ๋ฑํ๊ฒ ๋ง์ถ๋ ๋ฐ๋ฉด, text-pretty๋ ๋ ๊ฐ๋ฒผ์ด ์ต์ ํ์ผ. ๋ง์ง๋ง ์ค์ ๋จ์ด๊ฐ ํ๋๋ง ๋จ๋ "orphan" ์ํฉ๋ง ๋ฐฉ์งํด.
<!-- text-pretty: ๋ง์ง๋ง ์ค์๋ง orphan ๋ฐฉ์ง ์ต์ ํ -->
<!-- ๋ณธ๋ฌธ ํ
์คํธ์ฒ๋ผ ๊ธธ์ด๋ ์ฑ๋ฅ ์ด์ ์์ -->
<p class="text-pretty">
์์๋ค ์ปค๋ฎค๋ํฐ๋ ๊ฐ๋ฐ์๋ค์ด ํจ๊ป ๋ชจ์ฌ ์ฑ์ฅํ๋ ๊ณต๊ฐ์ด์์.
React, TypeScript, ์๊ณ ๋ฆฌ์ฆ ๋ฑ ๋ค์ํ ์ฃผ์ ์ ์คํฐ๋๋ฅผ ์์ ๋กญ๊ฒ
๋ง๋ค๊ณ ์ฐธ์ฌํ ์ ์์ด์. ์ง๊ธ ๋ฐ๋ก ์์ํด๋ณด์ธ์.
<!-- ๋ง์ง๋ง ์ค์ ๋จ์ด 1๊ฐ๋ง ๋จ๋ ์ํฉ ์๋ ๋ฐฉ์ง -->
</p>text-balance vs text-pretty ์ ํ ๊ฐ์ด๋:
| ์ํฉ | ์ถ์ฒ |
|---|---|
| h1, h2 ๊ฐ์ ์งง์ ์ ๋ชฉ | text-balance |
| ์นด๋ ๋ถ์ ๋ชฉ (2~3์ค) | text-balance |
| ๊ธด ๋ณธ๋ฌธ, ์ค๋ช ํ ์คํธ | text-pretty |
| ๋ธ๋ก๊ทธ ํฌ์คํธ body | text-pretty |
๐ข 3๋จ๊ณ: font-variant-numeric โ ์ซ์๋ฅผ ๋์์ธํ๋ค
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- ๋ฐ์ดํฐ ํ ์ด๋ธ์์ ์ซ์ ์ ๋ ฌ์ด ์ ๋ง๋ ์ด์ ์ ํด๊ฒฐ์ฑ ์ ์ดํดํ๋ค.
tabular-nums,slashed-zero,ordinal๋ฑ์ ์ค๋ฌด ์ ์ฉ ๋ฐฉ๋ฒ์ ์ ์ ์๋ค.
๋ฌธ์ : ์ซ์ ํญ์ด ๋ฌ๋ผ์ ์ธ๋ก ์ ๋ ฌ์ด ์ ๋ง์
์์๋ค ์ปค๋ฎค๋ํฐ ์คํฐ๋ ์ฐธ์ฌ ํํฉ:
์ฐธ์ฌ์ ์: 1,234๋ช
โ '1'์ด ์ข์์ ์ ๋ ฌ์ด ํ์ด์ง
๊ฒ์๊ธ ์: 89,012๊ฐ โ '8','9'๊ฐ ๋์ด์ ์์ผ๋ก ๋์ด
ํ์ฑ ์คํฐ๋: 456๊ฐ โ ...
์์์ ๋ ์ ๋ง์:
ํ์ : 4.9 โ ์์์ ์์น๊ฐ ์ ๊ฐ๊ฐ
ํ์ : 10.0 โ '10'์ด '4'๋ณด๋ค ๋์ด์ ์์์ ์์น๊ฐ ๋ค๋ฆ
tabular-nums โ ๋ชจ๋ ์ซ์ ํญ์ ๋์ผํ๊ฒ
<!-- โ ๊ธฐ๋ณธ ์ซ์ ํฐํธ: ๊ฐ ์ซ์์ ํญ์ด ๋ค๋ฆ (proportional) -->
<!-- 1์ ์ข๊ณ , 0์ ๋์ด์ ์์์ ์ ๋ ฌ์ด ์ ๋จ -->
<table>
<tr><td>1,234</td></tr>
<tr><td>89,012</td></tr>
</table>
<!-- โ
tabular-nums: ๋ชจ๋ ์ซ์(0~9)์ ํญ์ด ๋์ผ -->
<table class="tabular-nums">
<tr><td class="text-right">1,234</td></tr> <!-- '1' ํญ = '9' ํญ โ ์ ๋ ฌ ๋ง์! -->
<tr><td class="text-right">89,012</td></tr>
</table>slashed-zero โ 0๊ณผ O ํผ๋ ๋ฐฉ์ง
<!-- ๊ฐ๋ฐ์ ID๋ ์ฝ๋ ํ์์ ์ ์ฉ -->
<code class="slashed-zero font-mono text-sm bg-gray-100 px-1 rounded">
O0oO0 <!-- 0์ ๊ฐ์ด๋ฐ ์ฌ์ , O๋ ์์ด์ ๊ตฌ๋ถ ๋ช
ํ -->
</code>ordinal โ ์์ ํํ ์ต์ ํ
<!-- ์คํฐ๋ ์์ ํ์ -->
<span class="ordinal font-semibold text-indigo-600">1st</span>
<span class="ordinal font-semibold text-indigo-600">2nd</span>
<span class="ordinal font-semibold text-indigo-600">3rd</span>
<!-- ordinal: ํฐํธ์์ ์ ๊ณตํ๋ ํน์ ์์ ๊ธ๋ฆฌํ ์ฌ์ฉ -->๋ณตํฉ ์กฐํฉ
<!-- ๊ธ์ก ํ์ ํ
์ด๋ธ - slashed-zero + tabular-nums ์กฐํฉ -->
<dl class="space-y-2">
<div class="flex justify-between">
<dt>์คํฐ๋ ์ฐธ์ฌ๋น</dt>
<dd class="tabular-nums slashed-zero font-mono">โฉ50,000</dd>
</div>
<div class="flex justify-between">
<dt>ํ๋ซํผ ์์๋ฃ</dt>
<dd class="tabular-nums slashed-zero font-mono">โฉ5,000</dd>
</div>
<div class="flex justify-between font-bold border-t pt-2">
<dt>ํฉ๊ณ</dt>
<dd class="tabular-nums slashed-zero font-mono">โฉ55,000</dd>
</div>
</dl>font-variant-numeric ์ ์ฒด ๋ชฉ๋ก:
| ์ ํธ๋ฆฌํฐ | ํจ๊ณผ | ์ฃผ์ ์ฉ๋ |
|---|---|---|
tabular-nums | ๋ชจ๋ ์ซ์ ํญ ๋์ผ | ๊ธ์ก/ํต๊ณ ํ ์ด๋ธ ์ ๋ ฌ |
proportional-nums | ์ซ์ ํญ ์์ฐ์ค๋ฝ๊ฒ | ์ผ๋ฐ ๋ณธ๋ฌธ ์ซ์ |
lining-nums | ์ซ์๊ฐ ๋ชจ๋ baseline ์์ | ์ ๋ชฉ์ ์ซ์ |
oldstyle-nums | ์ผ๋ถ ์ซ์์ descender | ๊ณ ์ ์ ํธ์ง ๋๋ |
slashed-zero | 0์ ์ฌ์ | ์ฝ๋, ID ํ์ |
ordinal | ์์ ํน์ ๊ธ๋ฆฌํ | 1st, 2nd ํ๊ธฐ |
diagonal-fractions | 1/2 โ ํน์ ๋ถ์ ๊ธ๋ฆฌํ | ๋ ์ํผ, ์ํ ํํ |
๐ฌ 4๋จ๊ณ: font-feature-settings โ OpenType ๊ธฐ๋ฅ ์ ๊ธ ํด์
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
- OpenType feature๊ฐ ๋ฌด์์ธ์ง ์ดํดํ๊ณ , Tailwind๋ก ํ์ฑํํ๋ ๋ฐฉ๋ฒ์ ์ ์ ์๋ค.
font-variant-numeric์ดfont-feature-settings์ ๊ณ ์์ค ์ถ์ํ์์ ์ดํดํ๋ค.
OpenType Feature๋?
ํ๋ ํฐํธ(OpenType)์๋ ๊ธฐ๋ณธ๊ฐ์ผ๋ก ์จ๊ฒจ์ง ๋ค์ํ ๊ธ์ ๋ณํ์ด ๋ด์ฅ๋์ด ์์ด. ์ด๋ฅผ font-feature-settings๋ก ์ ๊ธ ํด์ ํ ์ ์์ด.
<!-- ๊ธฐ๋ณธ font-feature-settings ํ์ฑํ -->
<p class="font-features-['smcp']">Small Caps: ABC โ แดสแด</p>
<!-- smcp: Small Caps - ์๋ฌธ์๋ฅผ ์ํ ๋๋ฌธ์๋ก ๋ณํ -->
<p class="font-features-['onum']">Oldstyle Nums: 1234 โ ํด๋์ ์คํ์ผ ์ซ์</p>
<!-- onum: Oldstyle numerals - ํด๋์ ํธ์ง ์คํ์ผ ์ซ์ -->
<p class="font-features-['liga']">Standard Ligatures: fi, fl โ ํฉ์</p>
<!-- liga: Ligatures - fi, fl ๊ฐ์ ๊ธ์๊ฐ ์์ฐ์ค๋ฝ๊ฒ ์ฐ๊ฒฐ๋จ -->
<!-- ์ฌ๋ฌ feature ๋์ ํ์ฑํ -->
<p class="font-features-['smcp','tnum','onum']">
๋ณตํฉ OpenType ๊ธฐ๋ฅ ํ์ฑํ
</p>์์ฃผ ์ฐ์ด๋ OpenType Feature ํ๊ทธ:
| ํ๊ทธ | ์ด๋ฆ | ํจ๊ณผ |
|---|---|---|
smcp | Small Caps | ์๋ฌธ์๋ฅผ ์ํ ๋๋ฌธ์๋ก |
onum | Oldstyle Nums | ํด๋์ ์คํ์ผ ์ซ์ |
tnum | Tabular Nums | ๋ฑํญ ์ซ์ (tabular-nums์ ๋์ผ) |
liga | Ligatures | fi, fl ๋ฑ ํฉ์ |
calt | Contextual Alternates | ๋ฌธ๋งฅ๋ณ ๊ธ์ ๋ณํ |
case | Case-Sensitive Forms | ๋๋ฌธ์ ์ ์ฉ ๊ตฌ๋์ ์ต์ ํ |
์ฐธ๊ณ : tabular-nums, slashed-zero ๊ฐ์ font-variant-numeric ์ ํธ๋ฆฌํฐ๋ค์ ๋ด๋ถ์ ์ผ๋ก font-feature-settings๋ฅผ ์ฌ์ฉํ๋ ๊ณ ์์ค ์ถ์ํ์ผ. ๊ฐ๋ฅํ๋ฉด ๋์ ์์ค์ ์ ํธ๋ฆฌํฐ๋ฅผ ๋จผ์ ์ฐ๊ณ , ์์ ๋๋ง font-features-[...]๋ฅผ ์ง์ ์ฐ๋ ๊ฒ์ด ์ข์.
๐ 5๋จ๊ณ: @tailwindcss/typography โ Prose ํ๋ฌ๊ทธ์ธ ๋ง์คํฐ
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
@tailwindcss/typographyํ๋ฌ๊ทธ์ธ์ ์ญํ ๊ณผ ์ค์น ๋ฐฉ๋ฒ์ ์ดํดํ๋ค.proseํด๋์ค์ ์์์ด(modifier)๋ก ํฌ๊ธฐ, ์์, ๋คํฌ ๋ชจ๋๋ฅผ ์ปค์คํฐ๋ง์ด์งํ ์ ์๋ค.
์ prose ํ๋ฌ๊ทธ์ธ์ด ํ์ํ๊ฐ?
์์๋ค ์ปค๋ฎค๋ํฐ์ ๋งํฌ๋ค์ด์ผ๋ก ์์ฑํ๋ ์คํฐ๋ ๊ณต์ง์ฌํญ์ด๋ ๋ธ๋ก๊ทธ ํฌ์คํธ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค๊ณ ํด๋ณด์.
// Markdown์ HTML๋ก ๋ณํํ ๊ฒฐ๊ณผ๋ฅผ dangerouslySetInnerHTML๋ก ์ฝ์
export default function PostContent({ htmlContent }: { htmlContent: string }) {
return (
// โ ์คํ์ผ ์์: h1, h2, p, ul, code ๋ฑ์ด ๋ชจ๋ ๊ธฐ๋ณธ ๋ธ๋ผ์ฐ์ ์คํ์ผ
<div dangerouslySetInnerHTML={{ __html: htmlContent }} />
// ๊ฒฐ๊ณผ: ๋ณผํ์๋ ๋ HTML. ์ ๋ชฉ๋ ๋ณธ๋ฌธ๋ ๋ค ๊ฐ์ ํฌ๊ธฐ์ฒ๋ผ ๋ณด์
);
}Tailwind๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ชจ๋ HTML ์์๋ฅผ ๋ฆฌ์ (reset) ํด์ h1๊ณผ p๊ฐ ๊ฐ์ ํฌ๊ธฐ๋ก ๋ณด์ฌ. ์๋์ ์ธ ๋์์ธ์ด์ผ โ ๊ฐ๋ฐ์๊ฐ ์ง์ ์คํ์ผ์ ํต์ ํ๋๋ก. ํ์ง๋ง ๋งํฌ๋ค์ด ๋ ๋๋ง ์์ญ์์๋ ์ด๊ฒ ๋ฌธ์ ์ผ. h1์ ํฌ๊ฒ, h2๋ ์ค๊ฐ, code๋ ๋ชจ๋ ธ์คํ์ด์ค๋ก ๋ณด์ฌ์ผ ํ๋๋ฐ ์๋ฌด๊ฒ๋ ์์ผ๋๊น.
์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ @tailwindcss/typography ํ๋ฌ๊ทธ์ธ (prose ํด๋์ค)์ด์ผ.
์ค์น ๋ฐ ์ค์
# ์ค์น
npm install @tailwindcss/typography/* tailwind.css - Tailwind v4 ๋ฐฉ์ */
@import "tailwindcss";
@plugin "@tailwindcss/typography";// tailwind.config.js - Tailwind v3 ๋ฐฉ์
module.exports = {
plugins: [
require('@tailwindcss/typography'),
],
}prose ๊ธฐ๋ณธ ์ฌ์ฉ
// components/PostContent.tsx
// ๐ฆ ์ํธ: "๋งํฌ๋ค์ด ๋ ๋๋ง ์์ญ์ prose ํด๋์ค ํ๋๋ง ์ถ๊ฐํ๋ฉด ๋์ด์ผ"
export default function PostContent({ htmlContent }: { htmlContent: string }) {
return (
// prose: ๋ชจ๋ ํ์ HTML ์์์ ์ฝ๊ธฐ ์ข์ ํ์ดํฌ๊ทธ๋ํผ ์คํ์ผ ์๋ ์ ์ฉ
<div
className="prose"
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
// ๊ฒฐ๊ณผ:
// h1 โ ํฐ ์ ๋ชฉ (2.25em, ๊ตต๊ฒ, ์ฌ๋ฐฑ ํฌํจ)
// h2 โ ์ค๊ฐ ์ ๋ชฉ (1.5em, ๊ตต๊ฒ)
// p โ ์ ์ ํ line-height์ ์ฌ๋ฐฑ
// ul/ol โ ๋ค์ฌ์ฐ๊ธฐ์ ๋ง์ปค
// code โ ๋ชจ๋
ธ์คํ์ด์ค, ๋ฐฐ๊ฒฝ์
// blockquote โ ์ผ์ชฝ ๋ณด๋, ์ดํค๋ฆญ
// img โ ์๋ max-width
// a โ ์์๊ณผ ๋ฐ์ค
);
}prose ์์์ด (ํฌ๊ธฐ ๋ณํ)
<!-- ํฌ๊ธฐ ์์์ด: prose-sm, prose-base, prose-lg, prose-xl, prose-2xl -->
<div class="prose prose-sm">์ํ ํ์ดํฌ๊ทธ๋ํผ (๋ธ๋ก๊ทธ ๋ฉ๋ชจ, ๋ถ์ฐ ์ค๋ช
)</div>
<div class="prose">๊ธฐ๋ณธ (prose-base)</div>
<div class="prose prose-lg">๋ํ ํ์ดํฌ๊ทธ๋ํผ (์ฃผ์ ๋ธ๋ก๊ทธ ํฌ์คํธ)</div>
<div class="prose prose-xl">ํน๋ํ (๋ด์ค๋ ํฐ, ๋งค๊ฑฐ์ง ์คํ์ผ)</div>
<div class="prose prose-2xl">์ต๋ํ (ํ์ด๋ก ํ
์คํธ ์์ญ)</div>๋คํฌ ๋ชจ๋ ๋์
<!-- prose-invert: ์ด๋์ด ๋ฐฐ๊ฒฝ์ ๋ง๋ ๋ฐ์ ์์์ผ๋ก ์ ํ -->
<div class="prose dark:prose-invert">
๋คํฌ ๋ชจ๋์์ ์๋์ผ๋ก ํ
์คํธ์ ์์ ์์์ด ๋ฐ์ ํค์ผ๋ก ๋ฐ๋
</div>prose ์์ ์์์ด
<!-- prose-{color}: ๋งํฌ, ๊ฐ์กฐ ๋ฑ์ ํฌ์ธํธ ์์ ๋ณ๊ฒฝ -->
<div class="prose prose-indigo">์ธ๋๊ณ ํฌ์ธํธ ์์</div>
<div class="prose prose-violet">๋ณด๋ผ ํฌ์ธํธ ์์</div>
<div class="prose prose-emerald">์๋ฉ๋๋ ํฌ์ธํธ ์์</div>ํน์ ์์๋ง ์์ ํ๊ธฐ
<!-- prose-h2:text-indigo-600: h2 ์ ๋ชฉ ์์๋ง ์ธ๋๊ณ ๋ก -->
<!-- prose-a:no-underline: ๋งํฌ ๋ฐ์ค ์ ๊ฑฐ -->
<!-- prose-code:text-pink-500: ์ธ๋ผ์ธ ์ฝ๋ ์์ ๋ณ๊ฒฝ -->
<div class="prose
prose-h2:text-indigo-600
prose-a:no-underline
prose-a:font-medium
prose-code:text-pink-500
prose-code:bg-pink-50
prose-code:px-1.5
prose-code:rounded
">
<!-- ์ธ๋ฐํ๊ฒ ์ปค์คํฐ๋ง์ด์ฆ๋ prose ์์ญ -->
</div>์ค์ : ์์๋ค ์ปค๋ฎค๋ํฐ ์คํฐ๋ ๊ณต์ง์ฌํญ ์์ธ ํ์ด์ง
// app/studies/[id]/notice/[noticeId]/page.tsx
// ๐ฆ ์ํธ: "๊ณต์ง์ฌํญ ๋งํฌ๋ค์ด์ prose๋ก ๋ ๋๋งํด. ๋คํฌ ๋ชจ๋๊น์ง ํ ๋ฐฉ์."
import { marked } from 'marked'; // ๋๋ next-mdx-remote ๋ฑ
async function getNotice(noticeId: string) {
// ... DB์์ ๊ณต์ง์ฌํญ ๊ฐ์ ธ์ค๊ธฐ
return { title: "React ์คํฐ๋ 1ํ์ฐจ ๊ณต์ง", content: "## ์๊ฐ ์๋ด\n..." };
}
export default async function NoticePage({ params }) {
const notice = await getNotice(params.noticeId);
const htmlContent = marked(notice.content); // Markdown โ HTML ๋ณํ
return (
<article className="max-w-3xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{notice.title}</h1>
<time className="text-sm text-gray-500 mb-8 block">2025๋
1์ 15์ผ</time>
{/* prose๋ก ๋งํฌ๋ค์ด ๋ ๋๋ง ์์ญ ์คํ์ผ๋ง */}
<div
className="
prose prose-lg /* ๊ธฐ๋ณธ + ํฐ ํฌ๊ธฐ */
dark:prose-invert /* ๋คํฌ ๋ชจ๋ */
prose-indigo /* ๋งํฌ, ๊ฐ์กฐ์ ์ธ๋๊ณ */
prose-headings:font-bold /* ๋ชจ๋ ํค๋ฉ ๊ตต๊ฒ */
prose-code:text-sm /* ์ฝ๋ ํฐํธ ์๊ฒ */
max-w-none /* prose์ max-width ์ ํ ํด์ */
"
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
</article>
);
}๐ 6๋จ๊ณ: Fluid Typography โ ๋ทฐํฌํธ์ ๋ฐ์ํ๋ ๊ธ์ ํฌ๊ธฐ
๐ฏ ์ด ์น์ ์ ์ฝ๊ณ ๋๋ฉด:
clamp()CSS ํจ์์ ์๋ ์๋ฆฌ๋ฅผ ์ดํดํ๋ค.- Tailwind์์ ์์๊ฐ ๋ฌธ๋ฒ์ผ๋ก fluid typography๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ์ ์ ์๋ค.
๋ฐ์ํ ํฐํธ ํฌ๊ธฐ์ ๋ฌธ์ ์
<!-- โ ๊ธฐ์กด ๋ฐ์ํ ๋ฐฉ์: ๋ธ๋ ์ดํฌํฌ์ธํธ์์ ๊ฐ์๊ธฐ ํฌ๊ธฐ๊ฐ ๋ฐ๋ -->
<h1 class="text-2xl md:text-4xl lg:text-6xl">
๋ชจ๋ฐ์ผ: 24px โ 768px์์ ๊ฐ์๊ธฐ 36px โ 1024px์์ ๊ฐ์๊ธฐ 60px
(์ค๊ฐ ํฌ๊ธฐ ๊ธฐ๊ธฐ์์ ์ด์ํ ํฌ๊ธฐ)
</h1>CSS clamp() ํจ์
clamp(์ต์, ์ ํธ, ์ต๋) ํจ์๋ ์ธ ๊ฐ ์ค ํ์ฌ ์ํฉ์ ๋ง๋ ๊ฐ์ ์๋์ผ๋ก ์ ํํด:
- ๊ณ์ฐ๊ฐ์ด ์ต์๋ณด๋ค ์์ผ๋ฉด โ ์ต์ ์ฌ์ฉ
- ๊ณ์ฐ๊ฐ์ด ์ต์~์ต๋ ์ฌ์ด๋ฉด โ ๊ณ์ฐ๊ฐ ์ฌ์ฉ (๋ถ๋๋ฝ๊ฒ ๋ณํ)
- ๊ณ์ฐ๊ฐ์ด ์ต๋๋ณด๋ค ํฌ๋ฉด โ ์ต๋ ์ฌ์ฉ
/* ๊ฐ๋
: font-size๊ฐ ๋ทฐํฌํธ์ ๋น๋กํด ๋ถ๋๋ฝ๊ฒ ๋ณํจ */
font-size: clamp(1.5rem, 4vw, 4rem);
/*
๋ทฐํฌํธ 375px (๋ชจ๋ฐ์ผ):
4vw = 15px < 24px(1.5rem) โ 24px ์ฌ์ฉ (์ต์)
๋ทฐํฌํธ 600px:
4vw = 24px โ 24px ์ฌ์ฉ
๋ทฐํฌํธ 1200px:
4vw = 48px < 64px(4rem) โ 48px ์ฌ์ฉ
๋ทฐํฌํธ 2000px:
4vw = 80px > 64px(4rem) โ 64px ์ฌ์ฉ (์ต๋)
*/Tailwind์์ fluid typography ๊ตฌํ
<!-- Tailwind ์์๊ฐ ๋ฌธ๋ฒ์ผ๋ก clamp() ์ฌ์ฉ -->
<h1 class="text-[clamp(1.5rem,4vw,4rem)]">
๋ทฐํฌํธ์ ๋น๋กํด ๋ถ๋๋ฝ๊ฒ ์ปค์ง๋ ์ ๋ชฉ
</h1>
<!-- ๋ ์ ๊ตํ ๊ณ์ฐ: calc()์ vw ์กฐํฉ -->
<h2 class="text-[clamp(1.25rem,_2.5vw_+_1rem,_3rem)]">
<!-- _๋ ๊ณต๋ฐฑ ๋์ ์ฌ์ฉ (Tailwind ์์๊ฐ์์ ๊ณต๋ฐฑ ํ์ฉ ์ ํจ) -->
์ต์ 1.25rem, ์ต๋ 3rem, ์ค๊ฐ์ ์ ํ ๋ณด๊ฐ
</h2>tailwind.css์ fluid ํฌ๊ธฐ ๋ฑ๋ก (์ฌ์ฌ์ฉ)
/* tailwind.css - ํ๋ก์ ํธ ์ ์ฒด์์ ์ธ fluid ํฌ๊ธฐ ๋ฑ๋ก */
@theme {
/* Fluid Typography Tokens */
--text-fluid-sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem);
--text-fluid-base: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
--text-fluid-lg: clamp(1.125rem, 1rem + 0.625vw, 1.5rem);
--text-fluid-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.875rem);
--text-fluid-2xl: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
--text-fluid-3xl: clamp(1.875rem, 1.5rem + 1.875vw, 3.5rem);
--text-fluid-4xl: clamp(2.25rem, 1.75rem + 2.5vw, 4.5rem);
}<!-- ๋ฑ๋ก๋ fluid ํฌ๊ธฐ ์ฌ์ฉ -->
<h1 class="text-(length:--text-fluid-4xl) font-black">
์ด๋ค ๊ธฐ๊ธฐ์์๋ ์์ฐ์ค๋ฝ๊ฒ ํฌ๊ธฐ๊ฐ ์กฐ์ ๋๋ ํ์ด๋ก ํ์ดํ
</h1>
<p class="text-(length:--text-fluid-base)">
์ฝ๊ธฐ ํธํ ์ ๋์ ๋ณธ๋ฌธ ํ
์คํธ
</p>๐ ์ค์ : ์์๋ค ์ปค๋ฎค๋ํฐ ์คํฐ๋ ์นด๋ + ๊ฒ์๊ธ ์์ธ ํ์ด์ง
์์ฑํ ์คํฐ๋ ์นด๋ (๋ชจ๋ ํ์ดํฌ๊ทธ๋ํผ ๊ธฐ๋ฒ ์ ์ฉ)
// components/StudyCard.tsx โ ํ์ดํฌ๊ทธ๋ํผ ์ฌํ ๋ฒ์
interface StudyCardProps {
title: string;
description: string;
topic: string;
memberCount: number;
rating: number;
rank: number;
}
export default function StudyCard({ title, description, topic, memberCount, rating, rank }: StudyCardProps) {
return (
<div className="bg-white rounded-2xl shadow-md hover:shadow-xl transition-shadow duration-300 p-5 flex flex-col">
{/* ์์ ํ์ - ordinal + tabular-nums */}
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-indigo-600 bg-indigo-50 px-2 py-0.5 rounded-full">
{topic}
</span>
<span className="text-sm font-bold text-amber-500 ordinal tabular-nums">
{rank}์ {/* ordinal: ์์ ํ๊ธฐ ์ต์ ํ */}
</span>
</div>
{/* ์ ๋ชฉ - line-clamp + text-balance */}
<h3 className="
font-semibold text-gray-900 text-lg
line-clamp-2 /* 2์ค ๊ณ ์ */
text-balance /* ์ค ๊ท ํ ์ต์ ํ */
mb-2
">
{title}
</h3>
{/* ์ค๋ช
- line-clamp + text-pretty */}
<p className="
text-gray-600 text-sm leading-relaxed
line-clamp-3 /* 3์ค ๊ณ ์ */
text-pretty /* orphan ๋จ์ด ๋ฐฉ์ง */
flex-1
">
{description}
</p>
{/* ๋ฉํ ์ ๋ณด - tabular-nums */}
<div className="mt-4 pt-3 border-t border-gray-100 flex justify-between items-center">
<div className="text-sm text-gray-500">
<span className="tabular-nums font-medium text-gray-900">{memberCount}</span>๋ช
์ฐธ์ฌ
</div>
{/* ํ์ - tabular-nums + slashed-zero */}
<div className="flex items-center gap-1">
<span className="text-amber-400 text-sm">โ
</span>
<span className="text-sm font-medium tabular-nums slashed-zero">
{rating.toFixed(1)} {/* ์์์ ์๋ฆฌ ์ ๋ ฌ */}
</span>
</div>
</div>
</div>
);
}๊ฒ์๊ธ ์์ธ - prose ํ์ฉ
// app/studies/[id]/posts/[postId]/page.tsx
export default async function PostDetailPage({ params }) {
const post = await getPost(params.postId);
const htmlContent = marked(post.content);
return (
<div className="max-w-4xl mx-auto px-4 py-10">
{/* ํฌ์คํธ ํค๋ - fluid typography */}
<header className="mb-8">
<span className="text-sm font-medium text-indigo-600 mb-3 block">
React ์ฌํ ์คํฐ๋
</span>
{/* ์ ๋ชฉ - fluid + balance */}
<h1 className="
text-[clamp(1.75rem,3vw,3rem)]
font-black text-gray-900
text-balance
leading-tight mb-4
">
{post.title}
</h1>
{/* ๋ฉํ ์ ๋ณด */}
<div className="flex items-center gap-4 text-sm text-gray-500">
<time className="tabular-nums">{formatDate(post.createdAt)}</time>
<span className="tabular-nums">์กฐํ {post.viewCount.toLocaleString()}ํ</span>
<span className="tabular-nums">๋๊ธ {post.commentCount}๊ฐ</span>
</div>
</header>
{/* ๋ณธ๋ฌธ - prose */}
<div
className="
prose prose-lg
dark:prose-invert
prose-indigo
prose-headings:text-balance
prose-p:text-pretty
prose-code:text-sm
prose-pre:bg-gray-900
max-w-none
"
dangerouslySetInnerHTML={{ __html: htmlContent }}
/>
{/* ํต๊ณ ํ
์ด๋ธ - tabular-nums */}
{post.stats && (
<div className="mt-10 p-6 bg-gray-50 rounded-2xl">
<h3 className="font-bold text-gray-900 mb-4">์คํฐ๋ ์ฐธ์ฌ ํํฉ</h3>
<dl className="grid grid-cols-2 gap-4 sm:grid-cols-4">
{[
{ label: '์ด ์ฐธ์ฌ์', value: post.stats.members },
{ label: '์ถ์๋ฅ ', value: `${post.stats.attendanceRate}%` },
{ label: '๊ณผ์ ์๋ฃ', value: post.stats.homeworkDone },
{ label: 'ํ๊ท ์ ์', value: post.stats.avgScore.toFixed(1) },
].map(({ label, value }) => (
<div key={label} className="text-center">
<dd className="text-2xl font-bold text-indigo-600 tabular-nums slashed-zero">
{value}
</dd>
<dt className="text-xs text-gray-500 mt-1">{label}</dt>
</div>
))}
</dl>
</div>
)}
</div>
);
}๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
| ์ ํธ๋ฆฌํฐ | ์์ฑ๋๋ CSS | ์ฃผ์ ์ฉ๋ |
|---|---|---|
truncate | overflow: hidden; text-overflow: ellipsis; white-space: nowrap | 1์ค ๋ง์ค์ |
text-ellipsis | text-overflow: ellipsis | overflow ์์ ๋ ... ํ์ |
line-clamp-N | -webkit-line-clamp: N ์กฐํฉ | N์ค ๋ง์ค์ |
line-clamp-none | line-clamp ํด์ | ๋ฐ์ํ ํ ๊ธ |
text-balance | text-wrap: balance | ํค๋ฉ ์ค ๊ท ํ |
text-pretty | text-wrap: pretty | ๋ณธ๋ฌธ orphan ๋ฐฉ์ง |
tabular-nums | font-variant-numeric: tabular-nums | ์ซ์ ํ ์ด๋ธ ์ ๋ ฌ |
slashed-zero | font-variant-numeric: slashed-zero | 0 vs O ๊ตฌ๋ถ |
ordinal | font-variant-numeric: ordinal | ์์ ํ๊ธฐ |
font-features-[...] | font-feature-settings: ... | OpenType ๊ธฐ๋ฅ |
prose | ์ ์ฒด ํ์ดํฌ๊ทธ๋ํผ ์คํ์ผ ์ธํธ | ๋งํฌ๋ค์ด ๋ ๋๋ง |
prose-invert | ๋คํฌ ๋ชจ๋ prose ์์ | ๋คํฌ ๋ชจ๋ |
text-[clamp(...)] | font-size: clamp(...) | Fluid typography |
๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
Q1. ์์์ด๊ฐ "์คํฐ๋ ์นด๋ ์ ๋ชฉ์ด ์ด๋ค ์นด๋๋ 1์ค, ์ด๋ค ๊ฑด 3์ค์ด๋ผ ๊ทธ๋ฆฌ๋๊ฐ ๊นจ์ ธ์. ์ ํํ 2์ค๋ก ๊ณ ์ ํด ์ฃผ์ธ์"๋ผ๊ณ ์์ฒญํ๋ค. ์ด๋ค ํด๋์ค๋ฅผ ์ฌ์ฉํด์ผ ํ๋๊ฐ?
โ
์ ๋ต: line-clamp-2
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
:
line-clamp-2๋ ๋ด๋ถ์ ์ผ๋กoverflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;๋ฅผ ์์ฑํด.-webkit-line-clamp๋ ๋ฐ์ค ๋ด์์ ํ ์คํธ๋ฅผ ์ ํํ N์ค๋ก ์๋ผ์ฃผ๋ CSS ์์ฑ์ผ๋ก, ํ์ฌ ๋ชจ๋ ์ฃผ์ ๋ธ๋ผ์ฐ์ ์์ ์ง์๋ผ. - ์ค๋ต ํผ๋๋ฐฑ:
truncate๋white-space: nowrap์ผ๋ก 1์ค ๊ฐ์ ์ด๊ณ ,text-ellipsis๋ overflow ์ ... ํ์์ด์ง๋ง ์ค ์๋ฅผ ์ ํํ์ง๋ ์์. JavaScript๋ก ๋ฌธ์์ด์ ์๋ฅด๋ฉด px ๋จ์๋ก ๊ณ์ฐํด์ผ ํ๊ณ ๋ฐ์ํ ๋์์ด ๋ณต์กํด์ ธ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: 1์ค ์ ํ โ
truncate, N์ค ์ ํ โline-clamp-N. ๊ทธ๋ฆฌ๋ ์นด๋์ ๋์ด ๊ท ์ผํ๋ ํญ์line-clamp-*์ ์ญํ ์ด์ผ.
Q2. ์์๋ค ์ปค๋ฎค๋ํฐ ๋์๋ณด๋์ ์ฐธ์ฌ์ ์ ํต๊ณ๋ฅผ ํ ํํ๋ก ๋ณด์ฌ์ฃผ๋๋ฐ, ์ซ์๋ค์ ์์์ ์์น๊ฐ ๋ง์ง ์์์ ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง๋ค. ์ด๋ค Tailwind ์ ํธ๋ฆฌํฐ๋ก ํด๊ฒฐํ ์ ์๋๊ฐ?
โ
์ ๋ต: tabular-nums
๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: ์ผ๋ฐ ํฐํธ์ ์ซ์๋ ๊ธ์๋ง๋ค ํญ์ด ๋ค๋ฅธ proportional(๋น๋กํญ) ๋ฐฉ์์ด์ผ. ์๋ฅผ ๋ค์ด '1'์ ์ข๊ณ '0'์ ๋์ด. ์ด ๋๋ฌธ์ ์ธ๋ก๋ก ์ ๋ ฌ๋ ์ซ์๋ค์ ์์์ ์์น๊ฐ ๋ง์ง ์๊ฒ ๋ผ.
tabular-nums(font-variant-numeric: tabular-nums)๋ฅผ ์ ์ฉํ๋ฉด ๋ชจ๋ ์ซ์(0~9)์ ํญ์ด ๋์ผํ tabular(ํ ํฐํธ) ๋ฐฉ์์ผ๋ก ๋ฐ๋์ด ์์์ ์ด ์ ํํ ์ธ๋ก๋ก ์ ๋ ฌ๋ผ. - ์ค๋ต ํผ๋๋ฐฑ:
font-mono๋ฅผ ์ฐ๋ฉด ์ซ์ ์ ๋ ฌ์ ๋ง์ถ ์ ์์ง๋ง, ๋ชจ๋ ธ์คํ์ด์ค ํฐํธ ์ ์ฒด๋ก ๋ฐ๋์ด ๋์์ธ์ ์ํฅ์ ์ค.tabular-nums๋ ํ์ฌ ํฐํธ๋ฅผ ์ ์งํ๋ฉด์ ์ซ์ ํญ๋ง ์กฐ์ ํ๋ ๋ ์ ๊ตํ ํด๊ฒฐ์ฑ ์ด์ผ. - ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ๊ธ์ก/ํต๊ณ ํ
์ด๋ธ =
tabular-nums์ธํธ. ์ซ์ ์ปฌ๋ผ์ ํญ์tabular-nums text-right์กฐํฉ์ผ๋ก ๋ง๋ฌด๋ฆฌํด.
Q3. ์์๋ค ์ปค๋ฎค๋ํฐ์ ์คํฐ๋ ๊ณต์ง์ฌํญ์ ๋งํฌ๋ค์ด์ผ๋ก ์์ฑํด์ HTML๋ก ๋ ๋๋งํ๋ ๊ธฐ๋ฅ์ ์ถ๊ฐํ๋ค. h1, h2, p, ul, code ๋ฑ์ ์๋์ผ๋ก ์ฝ๊ธฐ ์ข์ ์คํ์ผ์ ์ ์ฉํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ๋๊ฐ?
โ
์ ๋ต: @tailwindcss/typography ํ๋ฌ๊ทธ์ธ ์ค์น ํ prose ํด๋์ค ์ ์ฉ
<div className="prose prose-lg dark:prose-invert prose-indigo max-w-none"
dangerouslySetInnerHTML={{ __html: htmlContent }} />๐ก ์์ธ ํด์ค:
- ์๋ฆฌ ์ค๋ช
: Tailwind๋ ๊ธฐ๋ณธ์ ์ผ๋ก Preflight(CSS reset)๋ก ๋ชจ๋ HTML ์์๋ฅผ ์ด๊ธฐํํด. h1๊ณผ p๊ฐ ๊ฐ์ ํฌ๊ธฐ์ฒ๋ผ ๋ณด์ด๋ ์ด์ ๊ฐ ์ด๊ฒ ๋๋ฌธ์ด์ผ.
@tailwindcss/typographyํ๋ฌ๊ทธ์ธ์ด ์ ๊ณตํ๋proseํด๋์ค๋ ์ด ์ด๊ธฐํ๋ฅผ ๋๋๋ ค์ HTML ๋ฌธ์ ์์๋ค์๊ฒ ํฉ๋ฆฌ์ ์ด๊ณ ์๋ฆ๋ค์ด ๊ธฐ๋ณธ ์คํ์ผ์ ๋ถ์ฌํด. ์ ๋ชฉ ๊ณ์ธต, ๋ฌธ๋จ ๊ฐ๊ฒฉ, ์ฝ๋ ๋ธ๋ก, ์ธ์ฉ๊ตฌ, ํ ์ด๋ธ ๋ฑ ๋งํฌ๋ค์ด์์ ์์ฑ๋๋ ๋ชจ๋ ์์๋ฅผ ์ปค๋ฒํด. - ์ค๋ต ํผ๋๋ฐฑ: ์ง์ CSS๋ฅผ ์์ฑํ๋ ๊ฒ๋ ๋ฐฉ๋ฒ์ด์ง๋ง, prose ํ๋ฌ๊ทธ์ธ์ ๋คํฌ ๋ชจ๋, ํฌ๊ธฐ ๋ณํ, ์์ ์์์ด, prose- ์ปค์คํฐ๋ง์ด์ง ๋ฑ ์์ญ ๊ฐ์ง ๊ธฐ๋ฅ์ด ์ด๋ฏธ ํฌํจ๋์ด ์์ด. ์ฌ๋ฐ๋ช ํ ์ด์ ๊ฐ ์์ด.
- ๐ ํต์ฌ ๊ธฐ์ต๋ฒ: ๋งํฌ๋ค์ด/HTML ๋ ๋๋ง ์์ญ โ
prose. ์ปค์คํฐ๋ง์ด์ง์prose-{element}:{class}ํํ. ๋คํฌ ๋ชจ๋๋dark:prose-invert๋ก ํ ๋ฐฉ์.
๐ฃ ์์ฒ ์ด์ ํด๊ทผ ์ผ๊ธฐ
์ค๋์ ํ์ดํฌ๊ทธ๋ํผ์ ๋ํด ๋ฐฐ์ ๋ค.
์์งํ ์ฒ์์ "๊ทธ๋ฅ font-size๋ font-weight ์๋๊ฐ?" ๋ผ๊ณ ์๊ฐํ๋ค.
๊ทธ๋ฐ๋ฐ ์์ ๋๋๊ฐ "์นด๋ ์ ๋ชฉ์ด 2์ค๋ก ๊ณ ์ ์ด ์ ๋๋ค"๊ณ ํ ๋,
๋ด๊ฐ ์ฒ์ ์ ์ํ ๊ฑด JavaScript๋ก ๋ฌธ์์ด์ ์๋ฅด๋ ํจ์์๋ค.์ํธ ํ์ด
line-clamp-2๋ฅผ ๋ณด์ฌ์คฌ์ ๋, ๋ ๊ทธ ๋ ๊ธ์์ ์ถฉ๊ฒฉ.๋ ์ถฉ๊ฒฉ์ ์ธ ๊ฑด
text-balance์๋ค. ์ ๋ชฉ ๋ง์ง๋ง์ ๋จ์ด ํ๋๋ง ๋ฉ๊ทธ๋ฌ๋ ๋จ๋ ๊ฒ
"orphan(๊ณ ์ ๋จ์ด)" ๋ผ๋ ํ์ดํฌ๊ทธ๋ํผ ์ฉ์ด๊ฐ ์๋ค๋ ๊ฒ, ๊ทธ๊ฑธ CSS ํ ๋จ์ด๊ฐ ํด๊ฒฐํ๋ค๋ ๊ฒ.ํ์ดํฌ๊ทธ๋ํผ๋ ๋จ์ํ ๊ธ์ ํฌ๊ธฐ๊ฐ ์๋๋ผ "๋ ์๊ฐ ์ผ๋ง๋ ํธํ๊ฒ ์ฝ์ ์ ์๋๊ฐ"๋ฅผ
ํฝ์ ๋จ์๋ก ์ค๊ณํ๋ ์์ ์ด์๋ค. ์์ ๋๋๊ฐ ์ ๊ทธ๋ ๊ฒ ๋ฏธ์ธํ ๋ถ๋ถ์๋ ์ ๊ฒฝ ์ฐ๋์ง
์ค๋ ์ฒ์์ผ๋ก ์กฐ๊ธ ์ดํดํ ๊ฒ ๊ฐ๋ค.๊ทธ๋ฆฌ๊ณ
proseํ๋ฌ๊ทธ์ธ... ๋งํฌ๋ค์ด ๋ ๋๋ง์ ์ด๊ฒ ์์๋ค๋ฉด ๋๋ h1๋ถํฐ blockquote๊น์ง
์์ญ ๊ฐ์ง CSS๋ฅผ ์ง์ ๋ค ์์ฑํด์ผ ํ์ ๊ฒ์ด๋ค.
์ด๋ฏธ ์ข์ ๋๊ตฌ๊ฐ ์๋๋ฐ ๋ชจ๋ฅด๊ณ ์ฒ์๋ถํฐ ๋ง๋๋ ๊ฒ, ๊ทธ๊ฒ ๊ฐ์ฅ ํผํด์ผ ํ ํจ์ ๊ฐ๋ค.
๐ ๋ ์์๋ณด๊ธฐ
- Tailwind CSS - line-clamp ๊ณต์ ๋ฌธ์
- Tailwind CSS - text-overflow ๊ณต์ ๋ฌธ์
- Tailwind CSS - font-variant-numeric ๊ณต์ ๋ฌธ์
- @tailwindcss/typography ๊ณต์ ๋ฌธ์
- MDN - CSS text-wrap: balance
- MDN - font-variant-numeric
- Fluid Typography ๊ณ์ฐ๊ธฐ - utopia.fyi
- CSS clamp() ์๋ฒฝ ๊ฐ์ด๋ - MDN