신호를 사용한 리액티브 프로그래밍 ‘솔리드’ 사용기
컨텐츠 정보
- 조회 481
본문
리액티브 상태 관리를 위한 솔리드js(Solid.js)만의 접근 방식은 다른 리액티브 프론트엔드 자바스크립트에 프레임워크에도 폭넓게 영향을 미쳤다. 솔리드는 상태가 아닌 신호(signal)를 기반으로 하는 간결한 리액티브 프리미티브 모음을 제공하고, 이를 활용해 고급 기능을 지원한다.
빠른 속도에 중점을 둔 간결한 코어 설계와 풍부한 엔터프라이즈 기능을 제공하는 솔리드는 2025년 1월 자바스크립트 현황(State of JavaScript) 설문조사에서 개발자 만족도 90%를 기록했다. Solid.js를 시작해보자.
솔리드스타트를 사용한 새 프로젝트 스캐폴딩
동급의 다른 프레임워크와 마찬가지로 솔리드 역시 서버 측 렌더링(SSR)과 같은 기능을 지원하는 풀스택 플랫폼을 제공한다. 또한 프로젝트 시작과 관리를 위한 명령줄 툴도 있다. 솔리드스타트(SolidStart)는 솔리드에서 새 프로젝트를 스캐폴딩하는 공식적인 방법이다.
최근 VS 코드에서 루 코드(Roo Code)와 제미나이(Gemini) 사용하는 방법을 리뷰했으니, 여기서는 이 AI 기반 스택을 활용해 솔리드 앱의 스캐폴딩을 생성하기로 했다. 또한 각 접근 방식을 비교하기 위해 수동 코딩도 진행했다. 필자의 요청에 따라 루 코드가 생성한 스캐폴딩은 다음과 같다.
npm create solid@latest -- --template typescript --name iw-solid-app이 명령은 템플릿과 타입스크립트를 사용해 새 프로젝트를 시작하는 용도로는 괜찮지만, 대화형 CLI 프롬프트를 처리할 만큼 AI가 똑똑하지는 않아서 직접 터미널로 들어가 질문에 답해야 했다. 그러나 이후 오류가 발생하면서 명령이 실패하는 바람에 평소에 하던 것처럼 npm create solid@latest를 실행하고 질문에 적절히 응답했다.
이와 같은 수동 코딩과 AI 지원 코딩의 혼합이 현대 개발의 추세인 것 같다. 기술이 어떻게 동작하는지를 실질적으로 이해해야 할 필요성은 앞으로도 한동안은 사라지지 않을 가능성이 높다. 결국 AI는 우리가 이미 알고 있는 것을 사용하는 또 하나의 방법일 뿐이다.
개발 모드에서 앱을 실행하려면 $ npm run dev를 입력한다.
이제 브라우저에서 http://localhost:3000으로 이동하면 다음 템플릿이 표시될 것이다.
개발 서버는 핫 모듈 교체(HMR)를 포함한 즉석 업데이트를 지원한다. 파일을 변경하면 브라우저에 표시되는 내용도 자동으로 업데이트된다.
리액티브 상태 관리에 신호 사용하기
다른 프레임워크와 마찬가지로 솔리드 역시 기능을 UI로 캡슐화하는 컴포넌트를 만들 수 있게 해준다. 또한 리액트와 마찬가지로 JSX를 템플릿 언어로 사용한다. 솔리드스타트가 제공하는 counter.tsx 컴포넌트를 보면 솔리드가 신호를 사용해 상태를 관리하는 방법을 이해할 수 있다.
import { createSignal } from "solid-js";import "./Counter.css";export default function Counter() { const [count, setCount] = createSignal(0); return ( setCount(count() + 1)} type="button"> Clicks: {count()} );}리액트가 useState를 쓸 자리에 솔리드는 createSignal을 사용한다는 점을 제외하면 리액트 컴포넌트에 거의 직접적으로 매핑된다. 내부적으로 신호는 상태와 다른 방식으로 작동한다. 신호는 솔리드가 DOM에 더 세부적으로 액세스할 수 있게 해주므로 엔진이 필요한 특정 노드만 업데이트할 수 있다. 또한 신호는 더 범용적인 연산자로 솔리드 전반에 사용되는 반면 useState는 컴포넌트 상태만 관리한다.
또한 여기 나오는 count와 같은 신호에 대한 액세스는 변수에 직접 액세스하는 방식이 아니라 게터 함수(count())를 호출하는 방식으로 이뤄진다는 것도 볼 수 있다.
신호와 effect
솔리드 컴포넌트는 함수 호출이므로 생성 시 한 번만 실행된다. 따라서 템플릿 외부에서 접근해야 하는 신호가 있는 경우 effect로 감싸야 한다. JSX 내에서 조금 전 count()로 한 것과 마찬가지로 신호 게터를 호출하면 변경 시 리액티브 값이 반환된다. 그러나 함수 본문에서는 effect를 사용해야 한다.
console.log("Count:",count()); // ❌ not tracked - only runs once during initialization.createEffect(()=>{ console.log(count()); // ✅ will update whenever `count()` changes.});// snippet from the docs따라서 useEffect는 신호를 위한 일종의 임시 옵저버 역할을 한다. 신호를 기반으로 템플릿 외부에서 이펙트를 수행해야 하는 경우 사용한다. createSignal, createEffect, 그리고 JSX의 네이티브 반응성을 조합하면 반응성의 기본적인 요소를 대부분 갖출 수 있다.
createResource로 원격 API 불러오기
솔리드는 신호의 기본적인 기능 위에 여러 기능을 계층으로 덧씌운다. 이러한 기능 중 하나인 createResource는 비동기 요청을 리액티브 방식으로 쉽게 처리할 수 있게 해준다. createSignal 위의 간단한 계층이다. 이것을 사용해서 Joke 컴포넌트를 만들어 Joke API에서 10개의 농담을 가져와 표시해보자.
먼저 index.tsx에 컴포넌트를 포함한다.
import Joker from "~/components/Joker";// …그 다음 components 디렉토리에 기초적인 불러오기 작업을 만든다.
import { createResource } from "solid-js";const fetchProgrammingJokes = async () => { const response = await fetch(`https://official-joke-api.appspot.com/jokes/programming/ten`); return response.json();};export default function JokerSimple() { const [jokes] = createResource(fetchProgrammingJokes); return ( Raw Jokes JSON
{JSON.stringify(jokes(), null, 2)} );}이 버전은 오류 처리 또는 디스플레이 로직이 없는 매우 단순한 버전이다. 핵심은 createResource 함수가 createSignal을 래핑해서 불러오기와 같은 비동기 작업을 매우 간단하게 만들어 준다는 점이다.
여기서 중요한 점은 fetchProgrammingJokes가 프로미스를 반환하고, createResource 호출은 jokes() 함수를 생성할 수 있게 해준다는 것이다. 이 함수를 템플릿에서 자유롭게 사용해 결과를 캡처할 수 있다.
솔리드의 관용구적 루프
이제 요소를 사용해 조금 더 보기 좋게 표시해 보자. 이는 솔리드의 관용구적 루프 처리 방식으로, map 함수형 연산자를 사용하는 방식보다 성능이 더 우수하다.
import { createResource, For } from "solid-js";//... {(joke) => ( {joke.setup} {joke.punchline}
)}는 Jokes() 프로미스가 이행(resolve)될 때 반환되는 배열과 같은 이터러블을 소비할 수 있게 해준다. 비동기 처리의 메커니즘에 대해서는 신경 쓸 필요가 없다. 루프 내에서 joke 이터레이터 객체에 액세스할 수 있다. 여기서는 이를 사용해 간단한 항목 리스트를 생성한다.
- API가 밥을 먹으러 간 곳은?
- RESTaurant.
Suspense 경계
컴포넌트를 사용하면 비동기 로딩에서 다양한 상태를 표시하는 경계를 쉽게 정의할 수 있다. 다음 예제에서 가 실제 어떻게 사용되는지 볼 수 있다.
import { createResource, For, Suspense } from "solid-js";// ...Fetching jokes... please wait!}> {(joke) => ( {joke.setup} {joke.punchline}
)}에 어떤 프로미스나 리소스를 대기하는지 명시적으로 알려줄 필요가 없다. 내부 비동기 작업의 이행을 자동으로 감지하고 대기하며, 그 사이 자리 표시자 콘텐츠를 표시한다. 이를 통해 불러오기 상태를 매우 쉽게 처리할 수 있다.
오류 경계
또 다른 일반적인 요구사항은 UI를 위한 오류 처리 경계를 정의하는 것이다. 솔리드는 이 작업도 간편하게 해준다.
import { createResource, For, Suspense, ErrorBoundary } from "solid-js"; ( Failed to load jokes!
Error details: {err.message}
)}> Fetching punchlines... please wait!}> {(joke) => ( {joke.setup} {joke.punchline}
)} 이 버전은 ErrorBoundary 컴포넌트로 래핑한다는 점을 제외하면 앞서 살펴본 버전과 동일하다. 이 컴포넌트는 비동기 호출에 문제가 발생할 경우 표시할 UI 영역을 간단히 정의할 수 있다(Jokes API에 의도적으로 오타를 넣어 테스트 가능).
솔리드의 이벤트 처리
인라인 디스플레이에서 펀치라인을 제거하고 설정에서 마우스 클릭을 캡처한 다음 알림에 펀치라인을 표시하는 방식으로 이벤트 처리를 테스트할 수 있다.
handleSetupClick(joke.punchline)} style={{ cursor: 'pointer', color: 'blue', textDecoration: 'underline' }} title="Click to reveal punchline"> Setup: {joke.setup}컴포넌트 함수 내에 정의된 이벤트 핸들러는 다음과 같다.
export default function JokerSimple() { const handleSetupClick = (punchline: string) => { alert(punchline); };//...이제 설정 라인을 클릭하면 onClick 속성에 의해 handleSetupClick이 실행된다. 이는 중괄호로 묶인 템플릿 식으로 정의된 익명 인라인 함수로, handleSetupClick 함수로 호출을 전달한다. 그러면 이 함수가 펀치라인이 포함된 알림을 연다. 펀치라인은 템플릿에서 joke.punchline에 액세스해 인수로 전달된다.
“프로그래머들이 모이기를 좋아하는 장소는 어디일까?”를 클릭하면 “푸 바(Foo Bar)”라는 답이 표시된다.
너무 재미없어서 오히려 웃긴 농담이다.
리액티브 체크박스
예를 들어 프로그래밍을 주제로 한 농담과 모든 농담 중에서 표시할 농담을 전환하는 기능을 추가하려는 경우를 상상해 보자. 원격 API는 URL 경로의 끝에서 두 번째 부분에 "programming"이라는 단어가 있는지 여부에 따라 이를 처리한다(즉, …/jokes/programming/ten 또는 …/jokes/ten).
사용자가 표시되는 농담을 전환할 수 있도록 페이지 맨 위에 체크박스를 추가한다. Counter 컴포넌트에서 리액티브 변수를 이미 살펴봤지만, 이 예제를 통해 더 면밀히 볼 수 있을 것이다. 우선 새 신호를 생성한다.
const [jokeType, setJokeType] = createSignal("");jokeType이라는 새 신호가 생성됐고 초기 값은 빈 문자열이다.
다음으로, 주 div의 헤드에 체크박스 요소를 삽입한다.
{setJokeType(jokeType()==''?'programming/':'')}}> checked와 onInput 속성은 솔리드 전용 속성이다. checked 속성은 토큰을 사용해 jokeType() 신호의 값을 "programming/"과 비교한다. 즉, jokeType 값이 "programming/"이면 체크박스가 선택된다. (체크박스에 boolean이 아닌 string을 사용한 이유는 createResource가 falsy 값에 대해 리액티브 업데이트를 트리거하지 않기 때문이다.)
onInput 속성은 체크박스의 입력 이벤트를 처리한다. 이벤트가 발생하면 jokeType 값을 변경한다. 즉, 빈 문자열과 "programming/" 사이를 전환한다. 이 바뀌는 값을 농담 불러오기의 URL에 사용할 것이다.
신호와 리소스 결합
이제 새 신호와 리소스를 결합한다. 이는 유도된 반응성의 한 유형으로, 리소스가 신호의 상태를 감시하고 그 상태에 따라 스스로를 업데이트한다. 그러면 해당 리소스를 감시하는 앱의 각 부분도 차례로 업데이트된다.
불어오기를 수행하는 프로미스 외에, createResource는 첫 인수로 소스 신호를 받는다. 이를 통해 여러 신호를 손쉽게 데이지 체인 방식으로 연결할 수 있다.
const [jokes] = createResource(jokeType, fetchJokes);이제 jokeType의 값이 변경될 때마다 jokes() 리소스도 종속 항목을 업데이트한다.
또한 농담 불러오기 함수도 소스 신호의 결과를 수신하므로 다음과 같이 새 값을 활용할 수 있다.
const fetchJokes = async (jokeType) => { return (await fetch(`https://official-joke-api.appspot.com/jokes/${jokeType}ten`)).json();}jokeType 신호는 fetchJokes의 인수에서 단순한 변수로 사용된다(jokeType 프로미스의 이행 결과물). fetch URL은 jokeType 값을 활용한다. 체크박스를 통해 신호가 변경되면 솔리드는 이를 감지하고 농담 목록을 자동으로 다시 불러온다(업데이트된 URL 사용).
결론
솔리드의 모든 기능을 한 번에 이해하려고 하면 엄두가 나지 않을 것이다. 그러나 솔리드는 이름처럼 믿을 수 있는 프레임워크이며 대부분의 요구사항을 충족하도록 잘 설계된 기능을 갖추고 있다. 솔리드를 익히는 좋은 방법 중 하나는 필요한 부분에 점진적으로 도입하는 것이다. AI 지원 코딩도 한 가지 방법이지만 이 방법은 가용한 모든 기능을 사용해서 복잡한 코드를 출력하는 경향이 있다. 개인적으로는 루 코드와 같은 툴에 과도하게 의존하기보다는 솔리드에 대해 충분히 파악한 후에 함께 사용하는 편이 더 낫다고 생각한다.
dl-itworldkorea@foundryco.com
관련자료
-
링크
-
이전
-
다음







