서버 측 렌더링이 쉬워진다…HTMX와 함께 하는 아스트로 활용법
컨텐츠 정보
- 조회 654
본문
아스트로.js(Astro.js)는 신중하게 설계된 강력한 풀스택 자바스크립트 플랫폼으로, 프론트 엔드와 백엔드에서 모두 유연한 기술 선택 옵션을 제공한다. 현재 깃허브에서 받은 별이 5만 개에 이를 정도로 반응이 좋다. 아스트로는 시작하기 위한 구조를 제공하는 동시에 사용할 기술에 대한 광범위한 옵션을 탐색할 수 있게 해준다.
이전 기사에서는 아스트로를 사용한 동적 웹 애플리케이션 개발의 기본을 소개했다. 여기서는 to-do 데모 애플리케이션의 코드와 빌드 프로세스를 더 심층적으로 살펴본다.
아스트로와 HTMX를 사용한 서버 측 렌더링
아스트로는 리액트(React), 뷰(Vue)와 같은 리액티브 프론트 엔드를 플러그인으로 지원하는 서버 측 렌더링(server-side rendering, SSR) 메타 프레임워크로 가장 유명하다. 사실 그 자체로 엔드포인트와 라우팅을 갖춘 강력한 백엔드 솔루션으로 발전해서 거의 모든 작업을 처리할 수 있다.
이번 데모에서는 HTMX 뷰 호스팅에 아스트로를 사용하면서 서버 측 성능을 살펴본다. 이 접근 방식에서 까다로운 동시에 흥미로운 부분은 응답에서 뷰 조각을 보낸다는 점이다. 아래에서 볼 수 있겠지만 이를 위해서는 약간의 트릭이 필요하다.
먼저 표준 아스트로 명령줄 툴을 사용해 새 애플리케이션을 시작한다(아스트로 CLI 설치 및 사용에 대한 정보는 아스트로.js 설명서 참조).
$ npm create astro@latest동적 엔드포인트를 사용할 것이므로 어떤 종류의 배포 어댑터를 사용해야 하는지 아스트로가 알아야 한다(어댑터에 대한 내용은 이전 기사 참조). 여기서는 노드(Node) 통합을 위한 어댑터를 사용한다.
$ npx astro add node
서비스
먼저 서비스 계층에서 맞춤형 코드를 빌드해 보자. 서비스 계층은 앱 전체에서 재사용 가능한 모든 미들웨어를 둘 중앙 장소를 제공한다. 실제 애플리케이션의 서비스 계층은 데이터 계층을 통해 데이터 스토어와 상호작용하겠지만, 여기서는 메모리 내 데이터만 사용할 수 있다.
아스트로에서는 이와 같은 용도로 /lib 디렉토리를 사용하는 것이 관례인 것 같다. 아스트로의 모든 코드는 /src 디렉토리에 위치하므로 서비스 코드는 src/lib/todo.js에 있다.
src/lib/todo.js:// src/lib/todo.jslet todosData = [ { id: 1, text: "Learn Kung Fu", completed: false }, { id: 2, text: "Watch Westminster", completed: true }, { id: 3, text: "Study Vedanta", completed: false },];export async function getTodos() { return new Promise((resolve) => { setTimeout(() => { resolve(todosData); }, 100); // Simulate a slight delay for fetching });}export async function deleteTodo(id) { return new Promise((resolve) => { setTimeout(() => { todosData = todosData.filter(todo => todo.id !== id); resolve(true); }, 100); });}export async function addTodo(text) { return new Promise((resolve) => { setTimeout(() => { const newTodo = { id: todosData.length+1, text, completed: false }; todosData.push(newTodo); resolve(newTodo); }, 100); });}주목해야 할 부분은 모든 함수가 프로미스를 반환한다는 것이다. 아스트로가 기본적으로 지원하는 이 기능은 네트워크 호출 차단을 방지하기 위해 데이터 스토어와 통신해야 하는 서비스 메서드 용도로 적합하다. 여기서는 타임아웃을 사용해 네트워크 지연을 시뮬레이션한다.
그 외에는 몇 가지 간단한 함수형 연산을 사용해서 todosData의 메모리 내 배열에서 필요한 작업을 수행하는 기본 자바스크립트 호출이다.
서비스 계층에 필요한 것은 이것이 전부다.
메인 뷰
이제 src/pages/index.astro에 대해 알아보자. astro create 명령은 간단한 시작 페이지를 만드는데, 이 페이지의 용도를 바꿀 수 있다. 먼저 Welcome 구성요소에 대한 모든 참조를 삭제하고 잠시 후에 빌드할 TodoList 구성요소를 사용한다.
----// src/pages/index.astroimport Layout from '../layouts/Layout.astro';import TodoList from '../components/TodoList.astro';---Layout> TodoList />Layout>
아스트로 구성요소(.astro 파일 내부에 정의됨)에는 “코드 중괄호”(----) 내부의 자바스크립트 코드와 HTML 기반 템플릿, 두 개의 세그먼트가 있다. 이는 다른 템플릿 기술과 비슷한 부분이지만 아스트로는 기본적으로 모든 요소가 서버에서 실행되고 최소한의 자바스크립트와 함께 HTML 번들로 패키징된 다음 클라이언트로 전송된다는 점에서 약간의 차이점이 있다.
재사용 가능한 구성요소
이제 TodoList 구성요소를 살펴보자. 구성요소 디렉토리에는 재사용 가능한 모든 .astro 구성요소가 들어 있으므로 TodoList는 src/components/TodoList.astro에서 찾을 수 있다.
---// src/components/TodoList.astroimport { getTodos, deleteTodo } from '../lib/todo';import TodoItem from './TodoItem.astro'; const todos = await getTodos();---div> form id="add-todo-form" hx-post="/api/todos" hx-target="#todo-list-container" hx-swap="beforeend"> input type="text" name="text" id="new-todo-text" placeholder="Add a new todo" required> button type="submit">Add Todobutton> form> ul id="todo-list-container"> {todos.map(todo => ( TodoItem todo={todo} /> ))} ul>div>style> #todo-list-container { background-color: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); max-width: 600px; margin: 20px auto; } /* Other styles ... */style>참고로 스타일 스니펫이 포함돼 있다. 여기서 볼 수 있듯이 아스트로를 사용하면 구성요소 범위의 CSS를 쉽게 포함할 수 있다. 이 글의 초점은 아스트로 애플리케이션의 구조와 논리에 있으므로 CSS에 대한 자세한 내용은 생략한다.
TodoList는 위에서 본 서비스 모듈에서 필요한 함수를 가져와서 뷰를 렌더링하는 데 사용한다. 먼저 await를 사용해서 to-do를 가져온 다음, 템플릿에서 todos.map을 사용해 반복한다. 각 Todo에 대해 TodoItem 구성요소를 사용하고 to-do 데이터가 포함된 속성을 전달한다. TodoItem 항목은 잠시 후에 살펴볼 것이다.
새 to-do를 만드는 데 사용되는 양식도 있다. HTMX를 사용해 백그라운드 AJAX와 함께 양식을 제출하고 페이지 리로드를 방지한다.
- hx-post : /api/all에 POST 요청을 제출하도록 지시한다.
- hx-target : 응답을 넣을 위치(to-do 목록 요소)를 나타낸다.
- hx-swap : 새 요소를 추가하는 방법을 세밀하게 조정한다(목록의 끝).
UI 부분 전체가 미리 서버에서 렌더링되고, 사전 패키징된 상태로 브라우저에 전송된다.
TodoItem 구성요소
생성 요청을 처리하는 API를 살펴보기 전에 src/components/TodoItem.astro에 있는 TodoItem 구성요소부터 보자.
---// src/components/TodoItem.astroimport { deleteTodo, getTodos } from '../services/todo';export interface Props { todo: { id: number; text: string; completed: boolean };}const { todo } = Astro.props;---li class="todo-item" id={`todo-${todo.id}`}> {todo.text} {todo.completed ? ' (Completed)' : ''} button class="delete-button" hx-delete={`/api/todos/${todo.id}`} hx-target="closest .todo-item" hx-swap="outerHTML"> Delete button>li>style> .todo-item { padding: 10px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }/*Other item styles ... */style>TodoItem은 앞서 살펴본 속성을 수락한다(타입스크립트 인터페이스를 포함한 아스트로 속성(props)에 대한 자세한 내용은 설명서 참조). props를 사용해서 to-do에 대한 간단한 목록 항목과 삭제 버튼을 만든다. 이 버튼은 HTMX를 사용해서 AJAX를 통해 삭제를 처리한다. 여기 나온 HTMX는 DELETE 요청을 제출하는 hx-delete를 사용한다. hx-target과 hx-swap 속성은 이러한 간단한 속성을 통해 HTMX의 강력함을 보여준다. 이렇게 해서 목록 항목 자체를 삭제 대상으로 지정할 수 있다. (hx-delete는 기본적으로 HTTP 성공 응답을 받으면 대상 요소를 제거한다.)
엔드포인트 삭제
다음으로, DELETE 요청을 처리하는 방법을 보자. 아스트로의 파일 기반 라우팅은 동일한 라우팅 의미체계를 사용해서 뷰와 함께 /pages 내부에 서버 엔드포인트를 정의할 수 있게 해준다. 여기서는 깔끔하게 정리하기 위해 /api 하위 디렉토리를 만들고, route 매개변수를 사용해서 삭제를 위해 제출된 todo ID를 캡처한다. 해당 파일은 src/pages/api/todos/[id].js다.
import { deleteTodo, getTodos } from '../../../lib/todo';export const prerender = false;export async function DELETE({ params, request }) { const id = parseInt(params.id, 10); if (isNaN(id)) { return new Response(null, { status: 400, statusText: 'Invalid ID' }); } await deleteTodo(id); return new Response(null, { status: 200 }); // Empty response is sufficient for delete}아스트로 엔드포인트는 템플릿 부분을 제외하면 뷰와 동일하다. 중요하게 볼 점은 prerender = false 플래그를 사용해서 엔진이 빌드할 때 이 엔드포인트를 만들지 않도록 한다는 것이다. 여기서 원하는 것은 동적 엔드포인트다. (아스트로 문서에는 어느 엔드포인트 함수를 동적으로 설정할지 조정하는 수단으로 getStaticPaths()가 언급된다. 여기서는 prerender를 사용해 모든 항목이 동적임을 나타낼 수 있다.)
이 엔드포인트는 DELETE로 표시되며 서비스 함수를 사용해 데이터에 대한 작업을 수행한다. 빈 응답 객체가 200 성공 코드와 함께 돌아오면 HTMX는 프론트 엔드에서 이를 받아 뷰에서 해당 항목을 제거한다.
todo 생성 엔드포인트
마지막으로 남은 중요한 퍼즐 조각은 todo 생성 엔드포인트 처리다. 새로운 todo 텍스트를 받아서 목록에 추가한 다음, 목록에 삽입할 마크업을 다시 보내야 한다.
대부분 경우 이 작업은 또 다른 서버 엔드포인트에서 수행된다. 다만 아스트로는 프로그래밍 방식으로 구성요소를 렌더링하는 기능을 개발하고 있다. (독립적인 컨테이너 API 렌더 구성요소에 대한 로드맵과 제안에서 이 작업의 진행 상황을 확인할 수 있다.)
지금은 비교적 간편한 해결 방법을 사용하고 페이지 뷰를 사용해 요청을 처리하고 TodoItem.astro 구성요소를 재사용해 응답 조각을 보낼 수 있는데, 이렇게 해서 코드의 DRY를 유지할 수 있다.
src/pages/api/todos/index.astro에 있는 유사 엔드포인트는 다음과 같다.
---import {addTodo} from '../../../lib/todo.js';import TodoItem from '../../../components/TodoItem.astro';let newTodo = null;if (Astro.request.method === 'POST') { const formData = await Astro.request.formData(); newTodo = await addTodo(formData.get('text'));}export const prerender = false;---구성요소 프론트 매터의 자바스크립트에는 요청에 대한 전체 액세스 권한이 있으므로 아무 문제 없이 메서드 유형에 따라 필터링이 가능하다. 일반적으로 자바스크립트는 완전한 서버 측 함수다. prerender = false를 사용해 다시 동적 구성요소를 나타내는 것도 볼 수 있다.
주 작업은 서비스 유틸리티의 create 함수를 사용해서 요청 양식 본문을 새로운 to-do 항목으로 바꾸는 것이다. 그런 다음 이를 TodoItem 구성요소의 prop으로 사용한다. TodoItem.astro가 새 항목 데이터를 사용하여 렌더링한 응답이 최종 결과가 된다.
실행
개발 모드에서 앱을 실행하려면 다음을 입력한다.
$ npx astro dev프로덕션 빌드를 생성하려면(/dist로 출력) 다음을 입력한다.
$ npx astro build아스트로.js 개발 경험에 대한 단상
아스트로는 다루기 쾌적하다. 개발 모드는 빠르고 꽤 안정적이며 수정된 부분만 다시 로드하는 부분도 뛰어나다. 또한 다음과 같이 브라우저에서 도움이 되는 오류를 표시하고 관련 문서에 대한 핫링크도 함께 제공한다.
이런 유형의 오류 보고는 개발자 경험에 상당히 신경을 쓴다는 것을 보여준다.
todo 생성 엔드포인트에서 일종의 예외적인 상황에 직면했지만 큰 어려움 없이 임시방편으로 해결이 가능했다. 또한 아스트로.js 프로젝트 측에서 공식적인 해결책을 만들기 위해 적극 노력하는 중이다.
이 분야에는 탄탄하게 자리를 잡은 우수한 프레임워크가 워낙 많아서 경쟁이 치열하지만 아스트로는 잘 해내는 중인 것 같다.
여기서 사용된 모든 데모 코드는 필자의 깃허브 리포지토리에서 볼 수 있다.
dl-itworldkorea@foundryco.com
관련자료
-
링크
-
이전
-
다음







