문제의 발단

부동소수점에 대해 복습하던 중, 웹 개발 주력 언어인 Javascript의 소수점 처리 방식이 궁금해졌다.

Typescript를 사용해본 독자들은 number 원시 타입에 익숙할 것이다.

그렇다면 number 타입은 어느 범위의 숫자를 지원할까?


Number

Javascript가 최대 및 최소로 표현할 수 있는 숫자는 MAX_VALUE, MIN_VALUE 속성으로 파악 가능하다.

console.log(Number.MAX_VALUE);
console.log(Number.MIN_VALUE);

JS Max, Min Value

 

즉, 해당 값을 넘어가면 오차가 발생하거나 infinity로 판단된다.

부동소수점에 대한 자세한 내용은 CS 파트에서 다루겠다.

그렇다면, 범위를 늘리려면 어떻게 해야할까? → `Bigint` 자료형을 사용해보자.


Bigint

BigIntNumber 원시 값이 안정적으로 나타낼 수 있는 최대치인 2^53 - 1보다 큰 정수를 표현할 수 있는 내장 객체이다.

 

BigInt는 정수 리터럴의 뒤에 `n`을 붙이거나(10n) 함수 `BigInt()`를 호출해 생성할 수 있다.

const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n

const hugeString = BigInt("9007199254740991");
// ↪ 9007199254740991n

속성

수학 연산자

BigInt는 대개 일반 숫자와 큰 차이 없이 사용할 수 있다.

console.log(1n + 2n); // 3

 

주의점

  1. 나눗셈 연산자에서는 소수부가 없다.
  2. `BigInt` 값과 일반 숫자를 섞어서 사용할 수 없다
    형 변환을 통해 자료형을 동일하게 설정해주어야 한다.
  3. `+` 연산자를 사용하면, `Number` 범위 외 비트는 잘려나간다.
    → 따라서, `bigint`는 `+` 연산자를 지원하지 않는다.
// #1
console.log(5n / 2n); // 2

// #2. 
console.log(1n + 2); // Error: Cannot mix BigInt and other types

// 형 변환
alert(1n + BigInt(2)); // 3
alert(Number(1n) + 2); // 3

// #3. + 연산자 사용 불가
alert( +2n ); // 에러

비교 연산자

  1. `>` `<` 모두 사요 가능하며, `Number`와 동일하게 작용한다.
  2. `Number` 자료형과 비교하는 경우, `==` 는 작동하나, `===` 는 false를 도출한다.
// #1. > <
console.log( 2n > 1n ); // true
console.log( 2n > 1 ); // true

// #2. == and ===
console.log( 1 == 1n ); // true
console.log( 1 === 1n ); // false

논리 연산자

`Number`와 동일하게 작동한다.

if (0n) {
  // 실행 X
}

console.log( 1n || 2 ); // 1 (truthy)
console.log( 0n || 2 ); // 2 (falsy)

Typescript

version 3.2 부터 `bigint`를 사용가능하다.

const num: bigint = 10n;

const num2: bigint = Bigint(10);

 

 


결론

그렇다면 항상 Bigint를 사용하는 것이 좋을까?

Bigint는 Number에 비해 연산과정이 오래걸린다.

시간적 소요는 성능에 영향을 끼치므로, 꼭 필요한 경우에만 사용하도록 하자.

프로젝트를 하다보면, 유저에게서 입력을 받아야 하는 상황이 다수 존재합니다.

대표적으로, 폼을 만들어 정보를 입력받는 상황이 있습니다.

이러한 폼에서는 제어 컴포넌트로 구성할지 비제어 컴포넌트로 구성할지에 대한 고민이 필요합니다.

다음의 2가지 Form 형식을 참고하여 제어 컴포넌트와 비제어 컴포넌트가 무엇인지 알아보겠습니다.


제어 컴포넌트

리액트 공식문서에서 제공하는 제어 컴포넌트의 정의

우리는 React state를 “신뢰 가능한 단일 출처 (single source of truth)“로 만들어 두 요소를 결합할 수 있습니다.
그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어합니다.
이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.

 

🔎 즉, State를 생성하여 폼에서 발생하는 값들을 관리하는 방식을 의미한다.

 

다음 예시 코드를 보면 이해가 쉽습니다.

function App() {
  const [input, setInput] = useState("");
  const onChange = (e) => {
    setInput(e.target.value);
  };

  return (
    <div>
      입력값
      <input onChange={onChange} />
    </div>
  );
}

export default App;

 

유저에게 입력을 받는 매우 간단한 코드입니다.

값이 변경될때마다, State로 정의한 input에 값을 반영합니다.

 

그렇다면, 비제어 컴포넌트는 무엇일까요?


비제어 컴포넌트

마찬가지로, 리액트 공식문서에서는 비제어 컴포넌트에 대해 다음과 같이 설명합니다.

비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다루어집니다.

모든 state 업데이트에 대한 이벤트 핸들러를 작성하는 대신 비제어 컴포넌트를 만들려면 ref를 사용하여 DOM에서 폼 값을 가져올 수 있습니다.

 

즉, 기존에 JQuery 혹은 Vanilla Javascript를 통해 DOM 객체에 직접 접근하여 값을 가져오는 방식을 의미합니다.

 

React에서는 useRef를 통해 DOM 객체에 직접 접근가능합니다.

다음 간단한 예시 코드를 살펴봅시다.

function App() {
  const inputRef = useRef(); // ref 사용
  const onClick = () => {
    console.log(inputRef.current.value);
  };

  return (
    <div className="App">
      <input ref={inputRef} />
      <button type="submit" onClick={onClick}>
        전송
      </button>
    </div>
  );
}
export default App;

 

해당 폼은 useRef Hook을 통해 사용자가 입력한 값을 폼 전송시에 접근합니다.

 

이렇게, 리액트에서의 제어 컴포넌트와 비제어 컴포넌트의 개념에 대해 알아보았습니다.

 


 

그렇다면 어떤 컴포넌트 형식을 언제 활용해야 할까요?

이를 알아보기 위해서, 먼저 각 방식의 장단점에 대해 분석해봅시다.

제어 컴포넌트의 장단점

장점

관리하는 값을 최신 상태 유지시킨다.

→ 유효성 검사 가능 (ex. 이메일 형식)

→ 실시간 피드백 제공 (ex. 비밀번호 복잡도)

 

단점

값이 바뀔때마다 리렌더링된다.

제어 컴포넌트에서의 단점은 생각보다 치명적입니다.

이전에 보았던 코드에서, 리렌더링 될때마다 콘솔로그를 찍어봅시다.

let render = 0;
function App() {
  const [input, setInput] = useState("");
  const onChange = (e) => {
    setInput(e.target.value);
  };

  const printRender = () => {
    console.log(render);
  };

  useEffect(() => {
    console.log(input);
    render++;
  }, [input]);

  return (
    <div>
      입력값
      <input onChange={onChange} />
      <button type="button" onClick={printRender}>
	        렌더링 횟수 출력
      </button>
    </div>
  );
}

export default App;

 

결과

 

이메일 하나 입력하는데 무려 20번의 Rerendering이 발생했습니다.

반면, 예상할 수 있듯이 비제어 컴포넌트는  Rerendering 횟수를 최소화할 수 있습니다.

마찬가지로, 기존 코드를 변형하여 봅시다.

import { useEffect, useRef } from "react";

let render = 0;
function App() {
  const inputRef = useRef(); // ref 사용

  const printRender = () => {
    console.log(render);
  };

  useEffect(() => {
    render++;
  }, [inputRef.current?.value]);
  return (
    <div className="App">
      입력값
      <input ref={inputRef} />
      <button type="button" onClick={printRender}>
        렌더링 횟수 출력
      </button>
    </div>
  );
}
export default App;

 

결과

비제어 컴포넌트의 렌더링

비제어 컴포넌트는 렌더링 횟수에서 제어 컴포넌트보다 우위에 있다고 이야기할 수 있습니다.

 


그렇다면 언제, 무엇을 사용해야 할까요?

장단점 분석의 뉘양스로는 제어 컴포넌트는 사용하지 않아야 할 것 같습니다.

하지만, 아닙니다.

무엇을 써야 할지는 상황에 따라 바뀝니다.

 

값을 추적하여 사용자에게 유효성 검사와 같은 기능을 제공해야 한다면 제어 컴포넌트를,

버튼을 클릭했을때 전체 유효성을 검사하는 방식을 선택해야 한다면 비제어 컴포넌트를 사용하면 됩니다.

 

물론, 하나의 폼에서 제어 컴포넌트와 비제어 컴포넌트를 동시에 사용하는 것도 괜찮습니다.

다만 그럴경우, 비제어 컴포넌트와 제어 컴포넌트는 컴포넌트화하여 렌더링되는 부분을 최소화하는 것이 좋겠습니다.

 


 

이상으로, 제어 컴포넌트와 비제어 컴포넌트가 무엇인지 알아보았습니다.

다음 글에서는 비제어 컴포넌트를 효율적으로 관리하는 React-Hook form 라이브러리 사용법에 대해 알아보겠습니다.

Zustand는 React 기반 프로젝트에서 전역상태를 관리하는 매우 유용한 라이브러리입니다. 가장 대중적으로 사용되던 Redux의 불편함을 뒤로하고, 편리하고 깔끔한 zustand가 npm trends에서 비등비등한 사용량을 보여주고 있습니다.
이에, Zustand가 어떠한 장점을 가지고 있고 사용하는 방법을 알아보는 시간을 가져보겠습니다.

 

(React CustomHook 및 상태 관리의 기본 개념에 대한 숙지를 전제한 상태로 작성되었습니다)

Redux를 뛰어넘은 zustand

Zustand의 장점

  1. Provider를 설정해줄 필요 없다.
  2. 보일러플레이트가 거의 zero에 가깝다.
  3. Redux와 같은 Flux Pattern을 사용하므로 친숙하다.
  4. Redux Devtools를 사용할 수 있다.

Zustand도 다른 상태관리 라이브러리와 마찬가지로, 상태를 구독하고 있는 Listener들을 저장합니다.

이후, 상태가 변경되었을 때 등록된 리스너들에게 상태가 변경되었다고 알려주며, 최신화된 상태값을 전달합니다.

(이를, 발행/구독 모델이라고 합니다)


사용방법

1. 설치하기

npm i zustand

2. Store 생성하기 (Store: 전역 변수)

  • Typescript에서는 `create<StoreType>` 을 명시해 주어야 합니다. (외 동일)
import { create } from 'zustand'

type State = {
  firstName: string
  lastName: string
}

type Action = {
  updateFirstName: (firstName: State['firstName']) => void
  updateLastName: (lastName: State['lastName']) => void
}

const usePersonStore = create<State & Action>((set) => ({
  firstName: '',
  lastName: '',
  updateFirstName: (firstName) => set(() => ({ firstName: firstName })),
  updateLastName: (lastName) => set(() => ({ lastName: lastName })),
}))

export default userPersonStore;

`

3. Component에서 사용하기

function App() {
  // "select" the needed state and actions, in this case, the firstName value
  // and the action updateFirstName
  const firstName = usePersonStore((state) => state.firstName)
  const updateFirstName = usePersonStore((state) => state.updateFirstName)
	// 전체 사용하는 방법
	const {firstName, lastName, updateFirstName, updateLastName} = usePersonStore();
  return (
    <main>
      <label>
        First name
        <input
          // Update the "firstName" state
          onChange={(e) => updateFirstName(e.currentTarget.value)}
          value={firstName}
        />
      </label>

      <p>
        Hello, <strong>{firstName}!</strong>
      </p>
    </main>
  )
}

 

Tip: 전체 스토어를 구독하지 않고, 필요한 부분만 구독하여 불필요한 렌더링을 최적화해야 한다.

 

끝입니다. 정말 간단하죠?
여러분의 프로젝트에서 Zustand를 적극 사용해보세요.

 


Deep Dive

어떤 라이브러리를 도입하기 전에는 코드 한번쯤은 파하쳐봐야 합니다. 겉모습만 보고 판단하면 안됩니다.

프로젝트에 치명적인 오버헤드를 발생시킬 수 있으니까요.

여기서부터는 zustand의 동작 원리에 대해 파헤쳐보는 내용입니다.

1. set함수 (setState)

Zustand 소스코드의 set함수는 다음과 같이 작성되어 있습니다.

 

현재 상태를 기반으로 새로운 상태를 리턴하는 함수 혹은, 아예 변경하려는 상태 값을 전달받습니다.

상태 변화는 값이 바뀌었을 때만 발생하며, `Object.assign`을 통해 변경됩니다.

// set function
let state; // 스토어 상태
const setState = (partial, replace) => {
    const nextState = typeof partial === 'function' ? partial(state) : partial;
    if (nextState !== state) {
      const previousState = state;
			// 값이 바뀐 경우에만 변화
      state = replace ? nextState : Object.assign({}, state, nextState);

      listeners.forEach(listener => listener(state, previousState));
    }
};

 

다음과 같이 실행할 수 있습니다.

store.setState(state => ({ counter: state.counter + 1 }));
// 혹은
store.setState({ counter: 10 });

 

🔎 Object.Assign을 사용하기 때문에 React의 setState에서 바뀌지 않는 기존 값을 보존하는 보일러 플레이트를 줄일 수 있다.

// React에서의 방식을 적용한다면
set((state) => ({ ...state, count: state.count + 1 }))

// Zustand에서는 바뀌지 않는 값은 기존 값이 된다.
updateFirstName: (firstName) => set(() => ({ firstName: firstName }))

 

🔥 주의 🔥
Object.Assign은 얕은 복사로 Nested Object를 바꿀때는 추가 Action이 필요합니다.

→ React의 setState처럼 `…`를 활용해야 하세요.

import { create } from 'zustand'

	const useCountStore = create((set) => ({
  nested: { count: 0 },
  inc: () =>
    set((state) => ({
      nested: { ...state.nested, count: state.nested.count + 1 },
    })),
}))

 

2. 상태의 구독

상태를 구독하는 함수를 등록할 때는 `subscribe` 함수를 사용합니다.

이 함수를 사용하여 모든 상태의 변화를 구독할 수도 있고, 상태의 일부만 구독할 수도 있습니다.

const subscribe = (listener, selector, equalityFn) => {
  if (selector || equalityFn) {
    return subscribeWithSelector(listener, selector, equalityFn);
  }
  listeners.add(listener);
  // 구독을 해제하는 함수도 리턴해준다.
  return () => listeners.delete(listener);
};

 

subscribe를 활용하여 다음과 같이 디버깅도 가능합니다.

const store = create(set => ({
  text: '',
  count: 0,
  setCount: newCount => set({ count: newCount }),
  increment: () => set(state => ({ count: state.count + 1 })),
  setText: text => set({ text })
}));

store.subscribe(state => console.log('Something's changed: ', state)); // 어떤 상태가 변경되더라도 로그가 출력됨
store.subscribe(
  state => console.log('Count is changed: ', count),
  state => state.count
); // count 값이 바뀔 때만 로그가 출력됨
store.subscribe(
  state => console.log('Text has been changed: ', text),
  state => state.text
); // text 값이 바뀔 때만 로그가 출력됨

store.setText('Changed'); // text 값만 변경
// 결과
// Something's changed: [Object]
// Text has been changed: Changed

 

이상으로, Zustand 사용방법이었습니다.

사용하기 쉬운 만큼 깃허브 코드도 Deep Dive 해볼만 한것 같습니다. 추후에 기여를 해보고 싶네요.

 


번외

Zustand는 Redux Devtools를 사용하여 개발이 가능합니다.

사용방법

  1. 먼저 Redux DevTools를 Chrome 웹 스토어에서 설치해줍니다.
  2. store를 devetools로 감싸서 사용합니다.
// store.js
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const store = (set) => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 })
})

const useStore = create(devtools(store))

export default useStore
  1. 애플리케이션을 브라우저로 띄운 다음 개발자 도구 창에서 Redux DevTools를 확인해보세요. store의 상태를 확인하실 수 있다.

DevTools 사용하지 않은 Production / 사용하는 Development 처리

// store.js
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'

const store = (set) => ({
  bears: 0,
  increasePopulation: () => set(state => ({ bears: state.bears + 1 })),
  removeAllBears: () => set({ bears: 0 })
})

// 
const useStore = create(
  **process.env.NODE_ENV !== 'production' ? devtools(store) : store**
)

export default useStore

참고 문헌

https://github.com/pmndrs/zustand

 

GitHub - pmndrs/zustand: 🐻 Bear necessities for state management in React

🐻 Bear necessities for state management in React. Contribute to pmndrs/zustand development by creating an account on GitHub.

github.com

 

React 상태 관리 라이브러리 Zustand의 코드를 파헤쳐보자

 

React 상태 관리 라이브러리 Zustand의 코드를 파헤쳐보자

TOAST UI Calendar의 새로운 상태 관리 방법을 도입하기 위해 참고한 라이브러리 Zustand의 코드를 분석해본다.

ui.toast.com

 

개쉽다! Zustand 사용법 - React 상태관리 라이브러리

 

개쉽다! Zustand 사용법 - React 상태관리 라이브러리

현재 리액트 상태관리 라이브러리는 참 많이 있습니다. 대표적인 Redux와 MobX, Recoil, Jotai, ... Redux가 상태관리 라이브러리의 시초격(Flux 패턴)이라 할 수 있는데요. 그렇기 때문에 많은 개발자들로

blacklobster.tistory.com

 

+ Recent posts