Template Fragments

Carson Gross

ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ(Template fragments)์€ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR) ํ…œํ”Œ๋ฆฟ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋น„๊ต์  ๋“œ๋ฌธ ๊ธฐ๋Šฅ์œผ๋กœ, ์ „์ฒด ํ…œํ”Œ๋ฆฟ์ด ์•„๋‹Œ ํ…œํ”Œ๋ฆฟ ๋‚ด์˜ ์ผ๋ถ€ ์กฐ๊ฐ(fragment) ๋˜๋Š” ๋ถ€๋ถ„ ์ฝ˜ํ…์ธ ๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค. ์ด ๊ธฐ๋Šฅ์€ ํ•˜์ดํผ๋ฏธ๋””์–ด ๊ตฌ๋™ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜(Hypermedia Driven Applications)์—์„œ ๋งค์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ์™œ๋ƒํ•˜๋ฉด ํ…œํ”Œ๋ฆฟ์˜ ์ผ๋ถ€๋ฅผ ๋ Œ๋”๋งํ•˜๊ธฐ ์œ„ํ•ด ๋ณ„๋„์˜ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜์ง€ ์•Š๊ณ , ํ…œํ”Œ๋ฆฟ์„ ๋‚ด๋ถ€์ ์œผ๋กœ ๋ถ„ํ•ดํ•˜์—ฌ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๊ฐœ๋ณ„ ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ์˜ ์ˆ˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  HTML์„ ํ•˜๋‚˜์˜ ํŒŒ์ผ์— ์œ ์ง€ํ•จ์œผ๋กœ์จ ๊ธฐ๋Šฅ์ด ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ๋” ์‰ฝ๊ฒŒ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ํ–‰๋™์˜ ์ง€์—ญ์„ฑ(Locality of Behavior) ์„ค๊ณ„ ์›์น™์„ ๋”ฐ๋ฅด๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

#๋™๊ธฐ ๋ถ€์—ฌ

Java์šฉ์œผ๋กœ ๋งŒ๋“ค์–ด์ง„ ์ž˜ ์•Œ๋ ค์ง€์ง€ ์•Š์€ ํ…œํ”Œ๋ฆฟ ์–ธ์–ด์ธ Chill Templates๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ์ด HDA๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ์–ด๋–ป๊ฒŒ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ ์—ฐ๋ฝ์ฒ˜๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๊ฐ„๋‹จํ•œ Chill ํ…œํ”Œ๋ฆฟ /contacts/detail.html์ด ์žˆ์Šต๋‹ˆ๋‹ค:

#/contacts/detail.html
<html>
    <body>
        <div hx-target="this">
          #if contact.archived
          <button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
          #else
          <button hx-delete="/contacts/${contact.id}">Archive</button>
          #end
        </div>
        <h3>Contact</h3>
        <p>${contact.email}</p>
    </body>
</html>

์ด ํ…œํ”Œ๋ฆฟ์—๋Š” ์•„์นด์ด๋น™ ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฐ๋ฝ์ฒ˜์˜ ์•„์นด์ด๋ธŒ ์ƒํƒœ์— ๋”ฐ๋ผ โ€œArchiveโ€ ๋˜๋Š” โ€œUnarchiveโ€ ๋ฒ„ํŠผ ์ค‘ ํ•˜๋‚˜๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. ์ด ๋‘ ๋ฒ„ํŠผ์€ ๋ชจ๋‘ htmx์— ์˜ํ•ด ๊ตฌ๋™๋˜๋ฉฐ, ์„œ๋กœ ๋‹ค๋ฅธ ์—”๋“œํฌ์ธํŠธ์— HTTP ์š”์ฒญ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ํ‘œ์‹œ๋œ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ๋•Œ, ํ•ด๋‹น ๋ฒ„ํŠผ์„ ๋‘˜๋Ÿฌ์‹ผ div์˜ ์ฝ˜ํ…์ธ ๋ฅผ ์—…๋ฐ์ดํŠธ๋œ ๋ฒ„ํŠผ์œผ๋กœ ๊ต์ฒดํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค. (div์˜ hx-target="this"๋ฅผ ์ฃผ๋ชฉํ•˜์„ธ์š”. ์ด๋Š” ๊ทธ div์˜ innerHTML์„ ๊ต์ฒด ๋Œ€์ƒ์œผ๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.) ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด โ€œArchiveโ€œ์™€ โ€œUnarchiveโ€ ์‚ฌ์ด๋ฅผ ๋ฒˆ๊ฐˆ์•„๊ฐ€๋ฉฐ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๋ถˆํ–‰ํžˆ๋„, ์ด ํ…œํ”Œ๋ฆฟ์˜ ๋‚˜๋จธ์ง€ ๋ถ€๋ถ„์ด ์•„๋‹Œ ๋ฒ„ํŠผ๋งŒ ๋ Œ๋”๋งํ•˜๋ ค๋ฉด, ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฒ„ํŠผ์„ ๋ณ„๋„์˜ ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌํ•˜๊ณ  ์ด ํ…œํ”Œ๋ฆฟ์— ํฌํ•จ์‹œ์ผœ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ง์ด์ฃ :

#/contacts/detail.html
<html>
    <body>
        <div hx-target="this">
          #include archive-ui.html
        </div>
        <h3>Contact</h3>
        <p>${contact.email}</p>
    </body>
</html>
#/contacts/archive-ui.html
#if contact.archived
<button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
#else
<button hx-delete="/contacts/${contact.id}">Archive</button>
#end

์ด์ œ ๋‘ ๊ฐœ์˜ ํ…œํ”Œ๋ฆฟ์ด ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค. ์ด์ œ archive-ui.html ํ…œํ”Œ๋ฆฟ์„ ๋ณ„๋„๋กœ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ด๋ ‡๊ฒŒ ๋ถ„๋ฆฌํ•˜๋ฉด ์•„์นด์ด๋น™ ๊ธฐ๋Šฅ์˜ ๊ฐ€์‹œ์„ฑ์ด ์ค„์–ด๋“ญ๋‹ˆ๋‹ค. detail.html ํ…œํ”Œ๋ฆฟ๋งŒ ๋ณด๊ณ  ์žˆ์„ ๋•Œ ๋ฌด์Šจ ์ผ์ด ์ผ์–ด๋‚˜๊ณ  ์žˆ๋Š”์ง€ ๋œ ๋ช…ํ™•ํ•ด์ง‘๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฐฉ์‹์œผ๋กœ ํ…œํ”Œ๋ฆฟ์„ ๊ทน๋‹จ์ ์œผ๋กœ ๋ถ„ํ•ดํ•˜๋ฉด ๊ด€๋ฆฌ ๋ฐ ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ค์šด ์ž‘์€ ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ๋“ค์ด ๋งŽ์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

#ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ์œผ๋กœ ๋ฌธ์ œ ํ•ด๊ฒฐํ•˜๊ธฐ

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด, Chill ํ…œํ”Œ๋ฆฟ์—๋Š” #fragment ์ง€์‹œ์–ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ง€์‹œ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ…œํ”Œ๋ฆฟ ๋‚ด์—์„œ ์ฝ˜ํ…์ธ ์˜ ์ผ๋ถ€ ๋ธ”๋ก์„ ์ง€์ •ํ•˜๊ณ  ๊ทธ ๋ถ€๋ถ„๋งŒ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

#ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ์„ ์‚ฌ์šฉํ•˜๋Š” /contacts/detail.html
<html>
    <body>
        <div hx-target="this">
          #fragment archive-ui
            #if contact.archived
            <button hx-patch="/contacts/${contact.id}/unarchive">Unarchive</button>
            #else
            <button hx-delete="/contacts/${contact.id}">Archive</button>
            #end
          #end
        </div>
        <h3>Contact</h3>
        <p>${contact.email}</p>
    </body>
</html>

์ด์ œ ํ…œํ”Œ๋ฆฟ ๋‚ด์— ์ด ์กฐ๊ฐ์ด ์ •์˜๋˜์—ˆ์œผ๋ฏ€๋กœ, ์ „์ฒด ํ…œํ”Œ๋ฆฟ์„ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ:

  Contact c = getContact();
  ChillTemplates.render("/contacts/detail.html", "contact", c);

ํ…œํ”Œ๋ฆฟ์˜ archive-ui ์กฐ๊ฐ๋งŒ ๋ Œ๋”๋งํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค:

  Contact c = getContact();
  ChillTemplates.render("/contacts/detail.html#archive-ui", "contact", c);

์ฒซ ๋ฒˆ์งธ ์˜ต์…˜์€ ์—ฐ๋ฝ์ฒ˜์˜ ์ „์ฒด ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋‘ ๋ฒˆ์งธ ์˜ต์…˜์€ ์•„์นด์ด๋ธŒ/์•„์นด์ด๋ธŒ ํ•ด์ œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋ฒ„ํŠผ๋งŒ ๋‹ค์‹œ ๋ Œ๋”๋งํ•˜๊ณ ์ž ํ•  ๋•Œ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

์กฐ๊ฐ์„ ์‚ฌ์šฉํ•˜๋ฉด, UI๋ฅผ ๋‹จ์ผ ํŒŒ์ผ์— ํ†ตํ•ฉํ•˜์—ฌ ์ด ๊ธฐ๋Šฅ์ด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์—ฌ๋Ÿฌ ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ์„ ์˜ค๊ฐ€๋ฉฐ ์ž‘์—…ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๊ธฐ๋Šฅ์˜ ๊ตฌํ˜„์ด ๋” ๋ช…ํ™•ํ•˜๊ณ  ๊นจ๋—ํ•ด์ง‘๋‹ˆ๋‹ค.

#์•Œ๋ ค์ง„ ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ ๊ตฌํ˜„

ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ(๊ทธ๋ฆฌ๊ณ  ์ด๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ง์ ‘ ๋ Œ๋”๋งํ•˜๋Š” ๊ธฐ๋Šฅ)์€ ํ…œํ”Œ๋ฆฟ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ๋น„๊ต์  ๋“œ๋ฌธ ๊ธฐ๋Šฅ์ด์ง€๋งŒ, htmx์™€ ๊ฐ™์€ ํ•˜์ดํผ๋ฏธ๋””์–ด ์ง€ํ–ฅ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์™€ ์ž‘์—…ํ•  ๋•Œ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์„ ํฌ๊ฒŒ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๊ธฐํšŒ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ์€ ํ…œํ”Œ๋ฆฟ ์กฐ๊ฐ ๊ฐœ๋…์ด ๊ตฌํ˜„๋œ ๊ฒƒ์œผ๋กœ ์•Œ๋ ค์ง„ ๋ช‡ ๊ฐ€์ง€ ์˜ˆ์ž…๋‹ˆ๋‹ค:

๋‹ค๋ฅธ ์˜ˆ๋ฅผ ์•Œ๊ณ  ์žˆ๋‹ค๋ฉด, ์—ฐ๋ฝํ•ด ์ฃผ์„ธ์š”. ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

</>