거기 잘생기신 분들, solid js 시식하고 가세요. - basic reactivity

jay·2022년 9월 17일
5

solid

목록 보기
1/2

#영상으로 보고 싶다면?
https://www.youtube.com/watch?v=B0zNmPyUXh0


안녕하세요, 단테입니다.
오늘은 solidjs의 기본 api에 대해 알아보겠습니다.
solidjs는 작년부터 눈여겨 보고있던 js프레임워크로 아직 현업에서는 적용된 케이스가 많지는 않지만 컴포넌트 내부에 선언한 함수가 한번만 실행되며 로직, 상태 업데이트를 리엑트보다 쉽고 직관적으로 인지할 수 있다는 점에서 더 나은 생산성을 전달해줄 것으로 기대되는 프레임워크입니다.

개인적으로 ai로 인해 FE개발자의 수요가 없어지지 않는다면 4년 이후에는 리엑트를 대체할 것이라고 생각합니다.

createSignal

signal은 solid에서 주요하게 사용하는 상태 객체입니다.
리엑트의 useState와 유사한 기능을 합니다. 컴포넌트 내부에서 조회하는 상태 값을 생성한다고 이해하실 수 있습니다.

import { createSignal } from "solid-js";

function createSignal<T>(
  initialValue: T,
  options?: { equals?: false | ((prev: T, next: T) => boolean) }
): [get: () => T, set: (v: T) => T];

createSignal 인자로는 어떤 자바스크립트 객체라도 들어갈 수 있습니다.
사용 형태를 보면 useState와 매우 유사한 것을 알 수 있는데요,

// read signal's current value, and
// depend on signal if in a tracking scope
// (but nonreactive outside of a tracking scope):
const currentCount = count();

// or wrap any computation with a function,
// and this function can be used in a tracking scope:
const doubledCount = () => 2 * count();

// or build a tracking scope and depend on signal:
const countDisplay = <div>{count()}</div>;

// write signal by providing a value:
setReady(true);

// write signal by providing a function setter:
const newCount = setCount((prev) => prev + 1);

solidjs의 api의 특징 중 하나로는 createSignal로 반환되는 signal의 값을
count와 같이 바로 참조하는 것이 아니라 count()와 같이 함수 호출을 통해 참조한다는 것입니다.

createSignal의 반환 타입을 보면 다음과 같습니다.

type Signal<T> = [get: Accessor<T>, set: Setter<T>];
type Accessor<T> = () => T;
type Setter<T> = (v: T | ((prev?: T) => T)) => T;

Accessor가 바로 그것으로 타입 T가 아닌 함수 타입인 것을 알 수 있습니다.

createSignal은 인자로 설정 객체를 선택적으로 받을 수 있습니다.
아래에서 인자인 equals는 setValue가 호출될 때 언제 signal이 업데이트 되었는지 판단하는 기준이 됩니다. 이때 비교는 이전 signal값과 setValue의 인자로 전달되는 값간의 javascript ===의 비교로 이뤄지는데요

// define equality based on string length:
const [myString, setMyString] = createSignal("string", {
  equals: (newVal, oldVal) => newVal.length === oldVal.length,
});

equals가 false일 경우 항상 업데이트 되었다고 판단합니다.

const [getValue, setValue] = createSignal(initialValue, { equals: false });

만약 signal이 javascript primitive 값이 아닌 object literal과 같은 객체라면 상태 조회에 대한 방식을 mutable하게 할지, immutable하게 할지를 결정할 수 있겠습니다.

// use { equals: false } to allow modifying object in-place;
// normally this wouldn't be seen as an update because the
// object has the same identity before and after change
const [object, setObject] = createSignal({ count: 0 }, { equals: false });
setObject((current) => {
  current.count += 1;
  current.updated = new Date();
  return current;
});

createEffect

createEffect는 JSX가 돔에 반영된 이후에(rendering phase) 최초 실행된다는 부분에서 리엑트의 useEffect와 유사합니다. 따라서 refs를 이용해 돔을 조작하는 등의 사이드 이펙트를 실행하기 적합합니다.

// signature
export declare function createEffect<Next>(fn: EffectFunction<undefined | NoInfer<Next>, Next>): void;

createEffect(() => doSideEffect(a()));

인자로 함수만 받을 경우에는 단순 sideEffect를 실행하는데 사용합니다. 이때 EffectFunction은 리턴타입이 주로 void일 것이라는 것을 추측할 수 있습니다.

리엑트의 useEffect와 다른 점

  • 리엑트의 useEffect와는 다르게 두번째 인자로 dependency array 를 받지 않습니다.
    내부에서 사용되는 signal 값에 의해 자동으로 dependency가 트래킹 됩니다.
    dependency array를 빼먹는 휴먼 에러를 린트를 쓰지 않고도 방지할 수 있어 장점입니다.
  • 첫 콜백함수가 아래 예제 코드와 같이 prev => typeof prev 타입으로 선언될 수 있으며, 두번째 인자로 prev로 들어올 첫번째 인자 값을 설정할 수 있습니다.
    useEffect와 다르게 따로 closure나 useRef를 선언하지 않고도 이전 effect의 값을 추적할 수 있는 수단을 제공하는 것은 큰 장점입니다.
export declare function createEffect<Next, Init = Next>(fn: EffectFunction<Init | Next, Next>, value: Init, options?: EffectOptions): void;

createEffect((prev) => {
  const sum = a() + b();
  if (sum !== prev) console.log("sum changed to", sum);
  return sum;
}, 0);

useEffect를 사용할 때 조심해야 하는 점과 마찬가지로 createEffect를 사용할 때 지양해야 하는 점을 공식문서에서 찾아볼 수 있습니다.

it's best to avoid setting signals in effects, which without care can cause additional rendering or even infinite effect loops
createEffect 내부에서 signal을 변경시키는 것은 지양해야 합니다. 불필요한 리렌더링이나 무한 루프를 야기할 수 있습니다.

잠깐, useEffect 사용할 때 무엇을 조심해야 하는지 잊어버렸거나 아직도 못봤다면!?
https://velog.io/@jay/you-might-need-useEffect-diet

useEffect 콜백 함수 내부에서 함수를 리턴하는 것으로 cleanup 함수를 작성했었다면 (setInterval과 같은 것을 정리하기 위해) createEffect 내부에서는 onCleanup 유틸 함수을 통해 명시적으로 cleanup 함수를 선언해주어야 합니다.

// listen to event dynamically given by eventName signal
createEffect(() => {
  const event = eventName();
  const callback = (e) => console.log(e);
  ref.addEventListener(event, callback);
  onCleanup(() => ref.removeEventListener(event, callback));
});

createMemo

Mobx의 computed, 리엑트의 useMemo와 유사한 기능을 제공합니다. 즉, signal의 변경에 따른 computation 결과 값을 리턴하는 함수를 readonly로 참조할 수 있게 해줍니다.

차이점은 값이 아닌 함수를 리턴하는 것입니다.

const value = createMemo(() => computeExpensiveValue(a(), b()));

// read value
value();

solid에서는 리엑트의 useState와 같이 값을 반환하는 것이 아닌 signal을 참조할 수 있는 함수를 반환하기 때문에 대부분의 경우 memo를 사용해야 할 일이 많이 일어나지 않습니다.

createSignal로 반환된 값과 createMemo로 반환된 값의 차이점이라면 의존성 변경에 따른 computed value가 컴포넌트 내부에서 여러 번 참조되는 경우입니다.

아래에서 username signal이 변경됨에 따라 user()가 jsx 내부에서 두 번 호출되나 searchForUser은 한번만 호출됩니다.

const user = createMemo(() => searchForUser(username()));
// compare with: const user = () => searchForUser(username());
return (
  <ul>
    <li>Your name is "{user()?.name}"</li>
    <li>
      Your email is <code>{user()?.email}</code>
    </li>
  </ul>
);

memo를 사용하지 않는다면, 조회할 때마다 searchForUser가 호출됩니다.
jsx 참조에서 참조 횟수에 비례하게 searchForUser가 호출되는 이유는 solidjs에서는 기본적으로 모든 상태 값을 참조할 때 함수호출이 필요하기 때문입니다.

다음과 같이 createMemo를 사용하지 않고 computedValue를 사용해야 하는 경우 user called가 두 번 호출됨을 알 수 있습니다.

const user = () => {
  console.log("user called");
  return searchForUser(username()));
}
// compare with: const user = () => searchForUser(username());
return (
  <ul>
    <li>Your name is "{user()?.name}"</li>
    <li>
      Your email is <code>{user()?.email}</code>
    </li>
  </ul>
);

// user called
// user called 

createMemo의 콜백 함수는 jsx에서 참조되지 않더라도 콜백 함수에서 참조하고 있는 변수가 변경되면 호출됩니다.

createMemo의 콜백함수는 순수함수로 작성되어야 하며, 다른 signal을 수정하면 안됩니다.

The memo function should not change other signals by calling setters (it should be "pure"). This enables Solid to optimize the execution order of memo updates according to their dependency graph, so that all memos can update at most once in response to a dependency change.

리엑트의 useMemo와 다른 점

앞서 createEffect와 동일하게 언제 값을 업데이트 할지에 대한 로직을 따로 작성해줄 수 있습니다. 콜백함수에서 참조하는 의존 값이 변경되더라도 === 비교를 통해 다를때만 업데이트하게 할 수 있습니다. 또한 이전 memo 값을 인자로 받을 수 있습니다.
const sum = createMemo((prev) => input() + prev, 0);

createResource

asyc request를 통해 변경되는 signal을 생성합니다.
createSignal의 비동기 버전이라고 보면됩니다.

import { createResource } from "solid-js";
import type { ResourceReturn } from "solid-js";

function createResource<T, U = true>(
  fetcher: (
    k: U,
    info: { value: T | undefined; refetching: boolean | unknown }
  ) => T | Promise<T>,
  options?: ResourceOptions<T, U>
): ResourceReturn<T>;

function createResource<T, U>(
  source: U | false | null | (() => U | false | null),
  fetcher: (
    k: U,
    info: { value: T | undefined; refetching: boolean | unknown }
  ) => T | Promise<T>,
  options?: ResourceOptions<T, U>
): ResourceReturn<T>;

createResource는 인자로 api 함fetcher 함수를 전달 받으며, fetcher가 응답을 받고 값을 리턴합니다.

createResource는 두 가지 시그니처가 있습니다.

const [data, { mutate, refetch }] = createResource(fetchData);
const [data, { mutate, refetch }] = createResource(sourceSignal, fetchData);

두번째 시그니처는, sourceSignal이 변경되었을 때 refetch됩니다.
리엑트에서는 다음과 같이 표현할 수 있습니다.

useEffect(() => {
  fetch().then(setState)
},[sourceSignal])

react-query의 useQuery와 형태가 유사하지 않나요?

하지만 여기서의 mutate는 server state를 변경하는 것이 아니라 직접적으로 data를 변경하는 용도로 사용됩니다.

createSignal의 signal 반환값과 동일하게 data()로 함수 실행하여 값을 참조할 수 있습니다.

차이점은 data.loading, data.error와 같이 http 통신 결과에 대한 메타 데이터를 조회할 수 있습니다.

async function fetchData(source, { value, refetching }) {
  // Fetch the data and return a value.
  //`source` tells you the current value of the source signal;
  //`value` tells you the last returned value of the fetcher;
  //`refetching` is true when the fetcher is triggered by calling `refetch()`,
  // or equal to the optional data passed: `refetch(info)`
}

const [data, { mutate, refetch }] = createResource(getQuery, fetchData);

// read value
data();

// check if loading
data.loading;

// check if errored
data.error;

// directly set value without creating promise
mutate(optimisticValue);

// refetch the last request explicitly
refetch();

오늘은 solidjs 공식 문서에 있는 내용 중 basic reactivity에 해당하는 api들을 살펴보았습니다.

읽어주셔서 감사합니다.

https://www.solidjs.com/guides/getting-started

profile
성장을 향한 작은 몸부림의 흔적들

1개의 댓글

comment-user-thumbnail
6일 전
답글 달기