React와 Solid의 차이점 톺아보기
Tags: solidjs, javascript, korean, react
Published: 2023-06-29 08:14:49

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

{% embed https://github.com/solidjs/solid %}

이걸로 잠시 관리자 페이지를 만들다가 **협업의 어려움**으로 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` 호출만 해도 상태를 바꾸는 옵션도 설정할 수 있다.

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

(만약 React 에서 같은 기능을 구현하려면, [최대한 랜덤 수를 세팅하는 `useState` 기반 함수](https://github.com/react-hookz/web/blob/master/src/useRerender/index.ts)를 만들든가, `@react-hookz/web` 유틸리티의 [`useRender`](https://react-hookz.github.io/web/?path=/docs/lifecycle-usererender--example) 훅을 이용하면 된다.)

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

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

## 부작용(Side-Effect)

### React

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

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

### Solid.js

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

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

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

## 범위 관리(Context)


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

## 동적 컴포넌트

### React

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

```jsx
export default function MyDynamicComponent({ tag, ...props }) {
  const Dynamic = tag
  return (<Dynamic {...props} />)
}
```

### Solid

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

```jsx
export default function MyDynamicComponent({ tag, ...props }) {
  return (<Dynamic as={tag} {...props} />)
}
```

## 조건문 / 반복문

### React

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

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

### Solid.js

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

```jsx
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>)
}
```

## 비동기 컴포넌트/Suspense

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

## 비동기 반응성

### React

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

### Solid.js

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

## ref

### React

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

```jsx
function RefComponent() {
  const myDiv = useRef()

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

  return (<div ref={myDiv}>뭐임마</div>)
}
```

### Solid.js

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

```jsx
function RefComponent() {
  let myDiv

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

  return (<div ref={myDiv}>뭐임마</div>)
}
```

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

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

```jsx
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));
}
```

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

```ts
declare module "solid-js" {
  namespace JSX {
    interface Directives {
      clickOutside?: () => void;
    }
  }
}
```

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

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

## 컴포넌트 속성

### React

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

```jsx
function MyPropsComponent({ some, children, ...others }) {
  return <div {...others}>
    {some && <h1>{some}</h1>}
    {children}
  </div>
}
```

### Solid

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

```jsx
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>
}
```

## 마치며

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

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