솔리드를 리엑트 처럼 작성하면 큰코 다친다구요?

dante Yoon·2022년 9월 18일
4

solid

목록 보기
2/2

영상으로 보고 싶다면?

https://www.youtube.com/watch?v=dNSXI3rBReo

안녕하세요, 단테입니다.
이전 시간에는 solid의 reactivity를 제공하는 기본 api들을 알아보았습니다.
오늘은 솔리드를 사용해 돔을 렌더링 할 때 꼭 알고 있어야 하는 점에 대해 알아보겠습니다.

Entry

리엑트와 마찬가지로 render 메소드를 사용해 solid 앱을 렌더링 시킬 엘리먼트를 지정합니다.

import { render } from "solid-js/web";

render(() => <App />, document.getElementById("main"));

솔리드의 reactivity를 정상적으로 작동하기 위해서는 render의 첫번째 인자는 react-dom과 다르게 <App/>이 아닌 () => <App/>으로 작성해야 합니다.

props

정적인 props는 리엑트의 JSX.Element를 작성하듯 동일하게 작성해주면 되지만 동적 props는 조심해야 하는 지점이 몇 개 있습니다.

static props

const Parent = () => (
  <section>
    <Label greeting="Hello">
      <div>John</div>
    </Label>
  </section>
);

dynamic props

리엑트에서는 props를 dot operator를 통해 props.name과 같이 참조하기 보다는 구조분해를 이용해 다음처럼 표현합니다.

// This is bad
// Here, `props.name` will not update (i.e. is not reactive) as it is destructured into `name`
const MyComponent = ({ name }) => <div>{name}</div>;

구조 분해 시 name은 dynamic props가 아니라 static props가 되어 버려 name이 상위 컴포넌트에서 변경되더라도 업데이트된 값을 참조할 수 없습니다.

그 이유는 솔리드의 props 구현이 일반 속성이 아닌 [getter]를 사용하기 때문인데요,

실제 코드는 살펴보지 않았지만, 왜 props.name과 같이 쓰면 안되는지 예상해본 대략적인 수도 코드입니다.

... 
<SomeComponent name={getName()}/>
                     
...

const ComponentWrapper = (Component, {name}) => {
  return () => {
    const props = {
       get() name{
         return getName();
       },
    }
	return Component(props);
  }
}

위에서 props는 Component 내부에서 props.name을 통해 직접 사용하는 시점에 맞춰서 name의 최신 값을 getName을 통해 불러옵니다.

props의 각 속성이 접근자로 선언이 되어있기 떄문에 구조분해를 해버리면 너무 일찍 호출됩니다.

다음과 같이 최대한 늦게 props의 속성 값을 참조해야 합니다.

// Here, `props.name` will update like you'd expect
const MyComponent = (props) => <div>{props.name}</div>;

이 부분은 mobx와 비슷하다고 생각합니다. Mobx도 너무 일찍 값을 불러오면 reactivity가 제대로 동작하지 않거든요.

솔리드가 props를 다루는 부분에서 다른 프레임워크와 차별화 되는 부분은 함수 컴포넌트는 리렌더링이 되는 것과 관계없이 오직 한번만 호출되는 것입니다.

그래서 아래와 같이 const value = props.value || "default"와 같이 컴포넌트 최상단에서 변수를 선언할 시 생각한 것 처럼 작동하지 않습니다. 컴포넌트의 render phase에 이미 props.value 접근자가 실행이 되어 props가 정적으로 참조하게 되어버렸기 때문입니다.
따라서 input value는 oninput 핸들러가 잘 동작함에도 BasicCompoennt의 값이 업데이트 되지 않습니다.

이러한 현상을 outside the observable scope 라고 합니다.

import { createSignal } from "solid-js";

const BasicComponent = (props) => {
  const value = props.value || "default";

  return <div>{value}</div>;
};

export default function Form() {
  const [value, setValue] = createSignal("");

  return (
    <div>
      <BasicComponent value={value()} />
      <input type="text" oninput={(e) => setValue(e.currentTarget.value)} />
    </div>
  );
}

구조분해를 못한다는 말은... default props도 못사용한다는 말인데?

리엑트를 사용할 떄는 다음 처럼 구조분해 시 default props를 선언해주는 것이 일반적입니다.
솔리드에서 이렇게 사용하면 안됩니다.

// 잘못된 방식
const BasicComponent = ({value = "default}) => {

  return <div>{value}</div>;
};

예제코드가 올바르게 동작하려면 다음과 같이 수정해야 합니다.

const BasicComponent = (props) => {
  const value = () => props.value || "default";

  return <div>{value()}</div>;
};

너무 불편한데?

매번 defaultProps를 따른 변수를 사용해 함수 형태로 작성하는 것은 매우 번거로운 일입니다. defaultProps가 10개라고 생각해보세요.

솔리드에서는 다음처럼 mergeProps라는 헬퍼 함수를 제공합니다.

const BasicComponent = (props) => {
  props = mergeProps({ value: "default" }, props);

  return <div>{props.value}</div>;
};

솔리드의 컴포넌트 사용 방식은 솔리드가 높은 퍼포먼스를 낼 수 있게 도와주는 주요 요인 중 하나입니다.
값을 바로 넘기는 것이 아닌, 실행 시점까지 lazy evaluation을 할 수 있게 함수를 넘기다보니 props가 돔에서 실제로 쓰이기 전까지 평가를 뒤로 미룹니다.

props의 reactivity를 망치지 않게 하는 helper 함수를 적절히 사용해야 DX(developer experience)를 유지하며 컴포넌트 작성이 가능한 것으로 보입니다.

// default props
props = mergeProps({ name: "Smith" }, props);

// clone props
const newProps = mergeProps(props);

// merge props
props = mergeProps(props, otherProps);

// split props into multiple props objects
const [local, others] = splitProps(props, ["class"])

children

리엑트와 children을 다루는 방식은 유사합니다. 단일 child는 props.children으로 단일 값을 조회할 수 있고, multiple child들은 각 child로 이뤄진 배열로 props.children을 조회합니다.

솔리드에서는 children을 더 쉽게 사용할 수 있는 유틸을 제공하는데요,
아래 예제에서는 List 컴포넌트의 children으로 세 개의 동일 게층의 JSX.Element들이 전달됩니다.

// multi child
const List = (props) => <div>{props.children}</div>;

<List>
  <div>First</div>
  {state.expression}
  <Label>Judith</Label>
</List>

map

For, each 문으로 각 child를 조작할 수 있습니다.

// map children
const List = (props) => <ul>
  <For each={props.children}>{item => <li>{item}</li>}</For>
</ul>;

children 값에 명령형으로 class를 추가할 때

const List = (props) => {
  // children helper memoizes value and resolves all intermediate reactivity
  const memo = children(() => props.children);
  createEffect(() => {
    const children = memo();
    children.forEach((c) => c.classList.add("list-child"))
  })
  return <ul>
    <For each={memo()}>{item => <li>{item}</li>}</For>
  </ul>;
}

children도 다른 props와 동일하게 구조분해를 진행하면 안됩니다.
솔리드는 VDOM을 사용하지 않아 children이 어떤 부분이 변경했는지에 대해 diffing algorithm을 수행하지 않습니다. 따라서 children을 조회해야 할 때는 위와 같이 children helper를 사용해 memo를 진행하는 것이 좋습니다.

오늘은 솔리드로 돔을 렌더링할 떄 유의해야 하는 점에 대해 알아봤습니다.
리엑트와 유사한 지점이 많아 렌더링과 관련된 부분은 건너뛰고 바로 코드부터 작성하게 된다면 생각했던 것 과 다르게 동작해 당황스러울 수 있는데요, 오늘 말씀드린 각 지점에서의 리엑트/솔리드의 차이점을 명확히 이해하시고 작성하신다면 큰 무리가 없을 것이라 생각합니다.

글 읽어주셔서 감사합니다 :)

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

2개의 댓글

comment-user-thumbnail
2022년 9월 19일

👍 리액트처럼 할려다 고생했던 기억이...

1개의 답글