DEV Community

Composite
Composite

Posted on • Updated on

React와 Solid의 차이점 톺아보기

또다른 JSX 기술 라이브러리, Solid.js가 있다.

GitHub logo solidjs / solid

A declarative, efficient, and flexible JavaScript library for building user interfaces.

SolidJS

Build Status Coverage Status

NPM Version Discord Subreddit subscribers

WebsiteAPI DocsFeatures TutorialPlaygroundDiscord

Solid is a declarative JavaScript library for creating user interfaces. Instead of using a Virtual DOM, it compiles its templates to real DOM nodes and updates them with fine-grained reactions. Declare your state and use it throughout your app, and when a piece of state changes, only the code that depends on it will rerun.

At a Glance

import { createSignal } from "solid-js";
import { render } from "solid-js/web";
function Counter() {
  const [count, setCount] = createSignal(0);
  const doubleCount = () => count() * 2;
  
  console.log("The body of the function runs once...");

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>
        {doubleCount(
Enter fullscreen mode Exit fullscreen mode

이걸로 잠시 관리자 페이지를 만들다가 협업의 어려움으로 React로 전환한 뼈아픔이 있었지만, Solid도 상당히 매력적인 라이브러리다.

그래서 이제서야 꺼내게 되었다. React와 Solid.js의 차이점을.

가상 DOM

기본적으로, 일단 Solid.js 들어본 적 있다면 이미 알고 있을 것이다.
React와 Vue 는 가상 DOM을 사용하는 대표적인 라이브러리다. 아, Angular도.
그러나 Preact, Solid.js, Svelte 등은 가상 DOM 없이 실제 DOM을 바로 렌더링한다.

그래서, 이들의 장단점을 정리하면 대충 이렇다.

가상 DOM

  • 장점: 렌더링 전의 자주 발생하는 변경사항을 빠르게 정리할 수 있다.
  • 단점: 렌더링 할때마다 가상DOM을 경유하여 실제DOM으로 옮기기 때문에 상대적으로 렌더링 속도가 느리다.
  • 적합한 케이스: 태그 변경이 많이 일어나는 데이터그리드, 차트 등의 업무 중심의 컴포넌트

가상 DOM 사용하지 않음

  • 장점: 렌더링 빈도가 적을 수록 성능이 빨라진다.
  • 단점: 렌더링 빈도가 많을 수록 성능이 느려진다.
  • 적합한 케이스: 태그 변경이 적게 일어나는 CMS, 쇼핑몰에 쓰는 컨텐츠 중심의 컴포넌트

따라서, 이 문제에 대해서는, 당신의 앱이 어느 쪽이 적당할 지는 직접 써보고 판단해야 할 문제인 것이기 때문에, 존중이니 취향해 드리겠다.

상태관리

리액트의 기본 상태관리 함수는 useState 이다.
Solid의 기본 상태관리 함수는 createSignal 이다.

이 둘의 차이점을 알아보자.

React 상태관리

상태관리 함수는 React 컴포넌트 함수 내에서만 호출이 가능하다.
(단, React 상태관리 결과물은 함수 외부에서도 호출 가능하다.)

React 상태관리는 빌드 과정을 거쳐 렌더링 상태의 기준을 정의한다.

Solid.js 상태관리

상태관리 함수들은 컴포넌트 내외 모두 호출이 가능하다.
Solid.js 상태관리 함수 호출부와 결과물 모두 빌드하지 않는다.
따라서, 스코프에 따라 유연한 상태관리 기준을 정의할 수 있다.
심지어, setter 호출만 해도 상태를 바꾸는 옵션도 설정할 수 있다.

// 두번째 인자에 옵션이 있는데, 아래처럼 하면 상태값이 같아도 상태가 바뀐다.
const [, render] = createSignal(undefined, { equals: false })
Enter fullscreen mode Exit fullscreen mode

(만약 React 에서 같은 기능을 구현하려면, 최대한 랜덤 수를 세팅하는 useState 기반 함수를 만들든가, @react-hookz/web 유틸리티의 useRender 훅을 이용하면 된다.)

여기서 주의사항! 솔리드의 상태관리 결과물은 대부분 함수다! 따라서 호출해야 한다! 애초에 시스템이 다른데, 리액트가 렌더링 시 컴포넌트 함수 본문를 갈아엎기 때문에 가능하지만, 솔리드는 함수 원형을 유지하고 상태관리가 별개로 작동하기 때문이다.

const [getter, setter] = createSignal()
setTimeout(() => setter('메롱'), 1000)
// react와 달리 getter 는 함수로 호출해야 한다!
return <div>{getter()}</div>
Enter fullscreen mode Exit fullscreen mode

부작용(Side-Effect)

React

설명 해봐야 손만 아프다. 리액트 개발자야 말 안해도 알지?
그리고 이 함수 리액트 컴포넌트 본문에서만 호출 가능한 것도 알제?

useEffect(() => {
  /* 상태 바뀌면 실행할 함수 */
  return () => { /* 정리(Cleanup)할 함수 */ }
}, [...영향받을 속성이나 상태])
Enter fullscreen mode Exit fullscreen mode

Solid.js

사실 사용법은 큰 차이 없고, 당연하겠지만 리액트와 동일하게 비동기로 동작한다.
사소한 차이점이 있는데, 이제 설명하겠다.
먼저, 컴포넌트 함수 외에서도 호출 가능하다.
그다음, 위 React 구문을 똑같이 적용한다는 기준으로,

createEffect(() => {
  /* 상태 바뀌면 실행할 함수 */
  onCleanup(() => { /* 정리(Cleanup)할 함수 */ })
}) // 위 함수 본문에 명시한 상태값들이 영향받을 때 호출된다!
Enter fullscreen mode Exit fullscreen mode

첫번째, 영향 받을 상태를 명시적으로 지정 가능한 React와 달리, Svelte처럼 부작용 함수 본문에 언급한 상태가 바뀔 때 함수가 실행된다. 따라서 영향받을 상태값을 따로 함수로 빼는 꼼수를 통해 영향받을 상태를 한정짓는 방식으로 해결할 수 있다.
두번째, useEffect 함수에서는 리턴 함수로 정리할 수 있지만, createEffect 는 새로운 상태관리 영역 방식으로 관리하여 onCleanup 같은 생명주기 함수를 컴포넌트와 별개로 사용할 수 있다.

범위 관리(Context)

둘의 차이 별로 없다. 기능도 딱히 차이점 없으니 그냥 쓰던대로 쓰면 된다.

동적 컴포넌트

React

사실상 다이렉트로 동적 컴포넌트 호출 가능하다. 하지만, 빌드가 필요하고, 컴포넌트 함수임을 인지해야 하기 때문에 상수를 선언 후 그 상수를 컴포넌트 함수로 사용하는 살짝 번거로운 방법을 써야 한다. (JSX,TSX 한정, JS/TS일 경우 그럴 필요 없이 컴포넌트 함수임이 확실하면 함수처럼 호출하거나, 태그일 경우 React.createElement 함수 쓰면 됨)

export default function MyDynamicComponent({ tag, ...props }) {
  const Dynamic = tag
  return (<Dynamic {...props} />)
}
Enter fullscreen mode Exit fullscreen mode

Solid

Vue나 Svelte 처럼 Solid 에서도 동적 컴포넌트를 정의하는 내장 컴포넌트인 <Dynamic> 컴포넌트를 사용한다.

export default function MyDynamicComponent({ tag, ...props }) {
  return (<Dynamic as={tag} {...props} />)
}
Enter fullscreen mode Exit fullscreen mode

조건문 / 반복문

React

리액트는 조건문과 반복문을 직접적으로 지원한다. 어자피 빌드하니까. 어자피 상태 바뀌면 본문 갈아 엎으니까~ 화성 갈끄니께~

function MyConditionalComponent({ isShow }) {
  return (<div>
    {isShow && '앱있음'}
  </div>)
}
function MyForeachComponent({ list }) {
  return (<ul>
    {list.map(row => (<li>{row.name}</li>)}
  </ul>)
}
Enter fullscreen mode Exit fullscreen mode

Solid.js

솔리드는 직접적인 스크립트 렌더링을 지원하지 않는다. 본문이 안갈아엎어지는데 될 리가... 그래서 솔리드는 조건문과 반복문을 컴포넌트로 지원한다.

function MyConditionalComponent({ isShow }) {
  return (<div>
    <Show when={isShow}>앱있음</Show>
  </div>)
}
function MyForeachComponent({ list }) {
  return (<ul>
    <For each={list}>{(row) => (<li>{row.name}</li>)}</For>
  </ul>)
}
Enter fullscreen mode Exit fullscreen mode

비동기 컴포넌트/Suspense

둘이 비슷하다. lazy 함수가 있고 <Suspense> 컴포넌트가 두 진영 다 내장되어 있는데, 그냥 뭐 React 따라갔다고 보면 된다. 어자피 JSX가 애초에 리액트를 위해 태어난 놈이니 JSX 쓰는 다른 것들이 어쩌겠니...

비동기 반응성

React

먼저 리액트는 기본적으로 상태관리가 내부적으로 비동기로 진행될 뿐, 직접적인 비동기에 대한 대응을 편리하게 지원하지 않는다. 그 때문에 이를 쉽게 해결하기 위해 많이들 찾는게 바로 TanStack Query, 구 react-query 많이들 썼을 것이다.

Solid.js

TanStack Query가 Solid를 지원하기 시작했지만, 솔리드 자체적으로 createResource 훅 함수를 통해 비동기를 대응할 수 있는 기본적인 유틸리티를 제공한다.

ref

React

리액트는 useRef 훅이 있다. 이녀석은 기본적으로 값이 바뀌어도 렌더링되지 않기 때문에, 컴포넌트 참조 뿐만 아니라 반응성이 없어야 되는 영역에서도 두루 쓰이고 있다.

function RefComponent() {
  const myDiv = useRef()

  useEffect(() => console.log(myDiv.current), []);

  return (<div ref={myDiv}>뭐임마</div>)
}
Enter fullscreen mode Exit fullscreen mode

Solid.js

솔리드는 따로 훅이 없고, 그냥 let 변수를 선언한 걸 그냥 뙇 걸기만 해도 된다. 함수 본문이 유지가 되기 때문에 가능한 일. Svelte와 비슷하다 생각하면 된다.

function RefComponent() {
  let myDiv

  onMount(() => console.log(myDiv));

  return (<div ref={myDiv}>뭐임마</div>)
}
Enter fullscreen mode Exit fullscreen mode

React 처럼 콜백으로 받을 수 있다. 하지만 Solid에서는 더 강력한 요소의 상태관리 기능을 지원하는데,

바로 use: 속성이다. 이건 Svelte에도 있는 기능인데, 하나의 상태관리 영역으로 취급하기 때문에, 여기서도 컴포넌트 초기화될 때 호출하는 onMount 함수와 컴포넌트 정리할 때 onCleanup 함수를 쓸 수 있다.
어자피 요소 받은 뒤 호출하기 때문에 onMount 함수 호출은 필요없고, removeEventListener 같이 정리가 필요할 경우에 onCleanup 함수의 콜백 인자에 구현하면 된다.

export default function clickOutside(el, accessor) {
  const onClick = (e) => !el.contains(e.target) && accessor()?.();
  document.body.addEventListener("click", onClick);

  onCleanup(() => document.body.removeEventListener("click", onClick));
}
Enter fullscreen mode Exit fullscreen mode

타입스크립트 한정해서 단점은, use: 속성은 컴포넌트 속성에 당연히 없고 정의할 수 없기 때문에, 타입 정의를 어딘가에 해줘야 한다.

declare module "solid-js" {
  namespace JSX {
    interface Directives {
      clickOutside?: () => void;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

만약 ref 콜백을 사용한다면, 해당 컴포넌트 함수 본문에 onCleanup 함수 호출하면 된다. 선택은 자유.

React와 달리 앙상한 생태계에서, Svelte와 같이 생태계 의존도를 줄일 수 있는 중요한 기능이라 하겠다.

컴포넌트 속성

React

이젠 언급하기도 지쳤다. 말했지? 상태 바뀌면 함수 본문 같아엎는다고.
그래서 얻을 수 있는 이점 중 하나가 바로 컴포넌트 속성을 분해해서 변수처럼 사용할 수 있다는 점 되겠다.

function MyPropsComponent({ some, children, ...others }) {
  return <div {...others}>
    {some && <h1>{some}</h1>}
    {children}
  </div>
}
Enter fullscreen mode Exit fullscreen mode

Solid

하지만 Solid는 상태가 바뀌어도 본문은 그대로 유지가 되기 때문에 React처럼 속성 분해하는 등의 편리한 구문을 썼다간 큰 낭패를 본다. 당장 볼 수 있는 일이라면, 초기 상태에만 한 번 반영하고 그 이후로는 아무런 미동도 없는 네 컴포넌트를 발견할 수 있다. 따라서, 객체를 분해하는 짓은 여기서는 하지 않는 것이 좋고, 만약 "범주별 분리"를 하고 싶다면, Solid에서 제공하는 splitProps 유틸리티를 써야 한다.
즉, 중요한 핵심은, 절대 개별 속성으로 분해하지 말 것!

function MyPropsComponent(props) {
  const [local, others] = splitProps(props, ["some", "children"]);
  return <div {...others}>
    <Show when={local.some}><h1>{local.some}</h1></Show>
    {local.children}
  </div>
}
Enter fullscreen mode Exit fullscreen mode

마치며

그밖에 더 있긴 한데 일단 핵심만 짚어줬다. 더 알아보고 싶으면 직접 비교해보라 내 손 아파서 이만.
끗.

2023-07-02: 부작용(Side-effect) 차이점 추가. 이 중요한 걸 빼먹다니...
2023-07-02: 컴포넌트 속성 차이점 추가. 이 또한 중요한 걸 빼먹다니...

Top comments (0)