반응형

.mjs Vs .js

원래 Node.js의 모듈 시스템은 commonjs를 사용했습니다(require과 module.exports를 사용하는..)

그러다가 ECMAscript 모듈 시스템이 표준이 되었고, Node.js는 이를 지원하게 되었습니다.

Node.js 는 .cjs 파일로 commonjs 모듈 시스템을 지원했고,

.mjs파일로 ECMAsript 모듈 시스템을 지원했습니다.

그러고 js파일은 둘 다 모두를 지원하게 되었습니다.

(default는 commonjs)

(package.json에 "type" :"module"쓰면 ecma 모듈 지원)

 

 

.JS

const b = require("./main2.js");

console.log(b.a);

받아오는 js

function hello() {
  return "hello";
}
const a = "a";
module.exports = {
  hello: hello,
  a: a,
};

보내주는 js

 

.mjs OR package.json에 타입 추가

import { hello, a } from "./main2.mjs";

console.log(`${hello()} ${a}`);

받아오는 js

function hello() {
  return "hellozz";
}
const a = "a";

export { hello, a };

보내주는 js

 

출처ㅣ https://kjs-dev.tistory.com/entry/JS-js-%EC%99%80-mjs?category=1025503?category=1025503

반응형

'Web > Js' 카테고리의 다른 글

ES6 Map(), Set()  (0) 2023.01.19
React 18의 useSyncExternalStore, Tearing 현상은 무엇인가?  (0) 2023.01.18
useEffect와 useLayoutEffect  (0) 2023.01.11
React.memo와 useMemo 차이점  (0) 2023.01.11
[React]setState Callback 함수 사용  (0) 2023.01.11
반응형

얼마 전부터 회사 업무를 진행할 때 본격적으로, 그리고 의식적으로 ES6 에 도입된 문법을 적용하고 있는데, 그중에서 가장 자주 활용하는 자료구조, Map 과 Set 에 대해 이야기해보려고 합니다. 이 글의 모티브는 상당부분 Mozilla 웹기술블로그에 기반합니다. 사내세미나에서 발표한 내용을 글로 정리했습니다.

Map

  • Map() 은 자바스크립트의 key-value 페어(pair) 로 이루어진 컬렉션
  • key 를 사용해서 value 를 get, set 할 수 있음
  • key 들은 중복될 수 없음: 하나의 key 에는 하나의 value 만
  • key 로 사용할 수 있는 데이터형: string, symbol(ES6), object, function >> number 는 사용할 수 없음에 주의!
// 새로운 map 을 만들고 map 에 key, value 엔트리를 추가
let me = new Map();
me.set('name', 'kevin');
me.set('age', 28);
console.log(me.get('age'); // 28// 대괄호를 사용해서 map 을 선언하는 방법
const roomTypeMap = new Map(
  [
    ["01", "원룸(오픈형)"],
    ["02", "원룸(분리형)"],
    ["03", "원룸(복층형)"],
    ["04", "투룸"],
    ["05", "쓰리룸"]
  ]
);// 새로운 map 을 만들고 그 데이터를 기존의 [key, value] 페어컬렉션으로 채움
let you = new Map().set('name', 'paul').set('age', 34);
console.log(you.get('name')); // 'paul'// has(): 주어진 key 가 존재하는지 확인
console.log(me.has('name')); // true// size: map 에 담겨진 엔트리의 개수를 조회
console.log(you.size); // 2// delete(): 엔트리를 삭제
me.delete('age');
console.log(me.has('age')); // false// clear(): 모든 엔트리를 삭제
you.clear();
console.log(you.size); // 0

<참고 1> Map 과 Object 비교

  • Object 의 key 는 string 과 symbol(ES6) 만 가능하지만, map 은 어떤 값도 가능
  • Object 에서는 크기를 추적해서 알 수 있지만, map 은 손쉽게 얻을 수 있음(size)

Set

  • Set() 은 value 들로 이루어진 컬렉션(“집합”이라는 표현이 적절)
  • Array 와는 다르게 Set 은 같은 value 를 2번 포함할 수 없음
  • 따라서 Set 에 이미 존재하는 값을 추가하려고 하면 아무 일도 없음
// 비어있는 새로운 set 을 만듬
let setA = new Set();// 새로운 set 을 만들고 인자로 전달된 iterable 로 인자를 채움
let setB = new Set().add('a').add('b');setB.add('c');
console.log(setB.size); // 3// has(): 주어진 값이 set 안에 존재할 경우, true 를 반환
// indexOf() 보다 빠름. 단, index 가 없음
console.log(setB.has('b')); // true// set 에서 주어진 값을 제거
setB.delete('b');
console.log(setB.has('b')); // false// set 안의 모든 데이터를 제거
setB.clear();
console.log(setB.size); // 0
  • <TODO> has() 는 indexOf() 보다 빠르다. 다만, index 이 존재하지 않기때문에 index 로 value 로 접근할 수 없다.
 

<참고 2> Spread 연산자

  • 이터러블 오브젝트(iterable object)의 엘리먼트를 하나씩 분리하여 전개
// string == iterable object
console.log([...'music']); // ['m', 'u', 's', 'i', 'c']

<참고 3> for 문들

  • for 문
let sampleArr = [1, 2, 3, 4, 5];
for (let i = 0, length = sampleArr.length; i < length; i++) {
  console.log(sampleArr[i]);
}
  • forEach: ES5 자바스크립트 배열 메서드
let sampleArr = [1, 2, 3, 4, 5];
sampleArr.forEach(v => console.log(v));
  • for-in: Object 를 순회하기 위한
let sampleObj = {
  a: 1,
  b: 'hello',
  c: [1, 2]
}
for (let key in sampleObj) {
  console.log(key);
  console.log(sampleObj[key]);
}
  • for-of: 배열의 요소들, 즉 data 를 순회하기 위한(string 도 가능)
let sampleArr = [1, 2, 3, 4, 5];
let (for value of sampleArr) {
  console.log(value);
}
 
  • 일반 객체(Object)는 iterable 하지 않다!
  • for-of 나 …(spread 연산자)를 사용할 수 없다!
  • for-in 으로나 순회할 수 있다.
 

Map 의 iterable object

  • map.keys(), map.values()
  • map 안의 key 혹은 value 들을 순회할 수 있는 iterable object 를 반환
let me = new Map().set('a', 1).set('b', 2);
console.log([...me.keys()]); // ['a', 'b']
console.log([...me.values()]); // [1, 2]
  • map.entries(), map.next()
  • map 안의 모든 엔트리들을 순회할 수 있는 iterable object 를 반환
let you = new Map().set('Seoul', 28).set('Tokyo', 26);
let iterObj = you.entries();
console.log(iterObj.next()); // {value: ['Seoul', 28], done: false}
console.log(iterObj.next()); // {value: ['Tokyo', 26], done: false}
console.log(iterObj.next()); // {value: undefined, done: true}
  • for-of, map.forEach();
  • forEach 의 경우, 인자 순서가 이상한데(key, value 순서가 반대) Array.prototype.forEach() 구문과 통일성을 유지하기 위함(value, index, array 순서인 것)
let we = new Map().set('car', 30).set('bus', 45);// for-of 로 map 순회하기
for (let [key, value] of we) {
  console.log(key + '^' + value);
}
// 차례대로 'car^30', 'bus^45' 출력// forEach 로 map 순회하기
we.forEach((value, key, map) => {
  console.log(key + '$' + value);
});
// 차례대로 'car$30', 'bus$45' 출력
  • 자바스크립트 배열 메서드에 존재하는 map, filter 메서드는 Map 에 존재하지 않는다. 하지만 아래와 같은 방식으로 우회해서 사용이 가능하다.
let me = new Map().set('a', 1).set('b', 2);// value 가 1 이상인 엔트리만 filtering 하기
let map1 = new Map(
    [...me]
    .filter(([k, v]) => v > 1)
);
console.log([...map1.entries()]) // [['b', 2]]// key 뒤에 'super' 문자열을 붙이고, value 에 1을 더하기
let map2 = new Map(
    [...me]
    .map(([k, v]) => [k + "super", v + 1])
);
console.log([...map2.entries()]) // [['asuper, 2], [bsuper, 3]]

Set 의 iterable object

  • set.values();
  • 기본적으로 Set 의 prototype 메서드로 keys() 는 존재하지 않고, values() 만 존재하지만, MDN 의 설명에 따르면, map 오브젝트와 동일하게 동작하기 때문에 Set.prototype.keys() 는 Set.prototype.values() 와 같은 결과
let setA = new Set();
setA.add('a');
setA.add('b');
setA.add('a');console.log([...setA.keys()]); // ['a', 'b']
console.log([...setA.values()]); // ['a', 'b']
  • set.entries();
let setB = new Set();
setB.add('Korea');
setB.add('Japan');
setB.add('China');let entries = setB.entries();console.log(entries.next()); 
// {value: ['Korea', 'Korea'], done: false}
console.log(entries.next()); 
// {value: ['Japan', 'Japan'], done: false}
console.log(entries.next()); 
// {value: ['China', 'China'], done: false}
console.log(entries.next()); 
// {value: undefined, done: true}
  • for-of, set.forEach();
let setC = new Set();
setC.add('Korea');
setC.add('Japan');
setC.add('China');for (let key of setC) {
  console.log(key);
}
// 차례대로 'Korea', 'Japan', 'China' 출력setC.forEach((v, k) => {
  console.log(v);
})
// 차례대로 'Korea', 'Japan', 'China' 출력

Set: 집합연산

스위프트 집합연산 — 링크
  • union(합집합), intersection(교집합), difference(차집합)
let setA = new Set([1, 2, 3, 4, 5]);
let setB = new Set([4, 5, 6, 7, 8]);// 합집합
let unionSet = new Set([...setA, ...setB])
for (let value of unionSet) {
  console.log(value);
}
// 차례대로 1, 2, 3, 4, 5, 6, 7, 8 출력// 교집합
let intersectionSet = new Set(
  [...setA].filter(v => setB.has(v))
);
for (let value of intersectionSet) {
  console.log(value);
}
// 차례대로 4, 5 출력// 차집합
let differenceSet = new Set(
  [...setA].filter(v => !setB.has(v))
);
for (let value of differenceSet) {
  console.log(value);
}
// 차례대로 1, 2, 3 출력
  • symmetricDifference
// Symmetric Difference
var set1 = new Set([1, 2, 3, 4, 5]);
var set2 = new Set([3, 4, 5, 6, 7]);var symmetricDifferenceSet = new Set(
[...[...set1].filter(x => !set2.has(x)), ...[...set2].filter(x => !set1.has(x))]
)
for (let value of symmetricDifferenceSet) {
  console.log(value);
}
// 차례대로 1, 2, 6, 7 출력
 

지금까지 Map 과 Set 에 대해 자세히 알아보았습니다.

단순히 key 와 value 를 set 하거나 value 를 set 하는 것뿐만 아니라, iterable object 의 특성을 살려서 map 과 set 을 순회하는 것을 알아보았습니다. 더 나아가서는 map 과 set 에서는 지원하지 않는 배열 메서드(Array.prototype) 인 map, filter 를 적용해보고, 집합연산까지도 진행해보았습니다.

억지로 배열의 형태로, 기본 객체형태로 코딩하기 보다는 적재적소의 자료구조의 특성에 맞게 코딩하는 습관을 기르면 좋겠습니다.

다음에는 윗글에서도 잠깐 나왔지만, 왜 배열의 indexOf 메서드보다 Set 의 has 가 더 “빠른지" 알아보겠습니다^^ 읽어주셔서 감사합니다.

 

출처:https://medium.com/@hongkevin/js-5-es6-map-set-2a9ebf40f96b

반응형

'Web > Js' 카테고리의 다른 글

JS .js vs .mjs  (0) 2023.01.27
React 18의 useSyncExternalStore, Tearing 현상은 무엇인가?  (0) 2023.01.18
useEffect와 useLayoutEffect  (0) 2023.01.11
React.memo와 useMemo 차이점  (0) 2023.01.11
[React]setState Callback 함수 사용  (0) 2023.01.11
반응형

React18의 Tearing현상과 이를 해결하기 위한 useSyncExternalStore에 대해 알아봅시다.

원문 : https://blog.saeloun.com/2021/12/30/react-18-usesyncexternalstore-api

 

Meet the new hook useSyncExternalStore, introduced in React 18 for external stores

Ruby on Rails and ReactJS consulting company. We also build mobile applications using React Native

blog.saeloun.com

useSyncExternalStore API에 대해 알아보기 전에
이 새로운 훅을 이해하기에 유용한 용어에 익숙해지도록 하겠습니다.

Concurrent rendering and startTransition API

동시성은 작업의 우선 순위를 지정하여 여러 작업을 동시에 실행하는 메커니즘입니다.
이 개념은 Dan Abramov가 전화 통화에 비유하여 쉽게 설명합니다.

 

set~함수를 통한 상태 업데이트를 리액트가 지연 처리할 수 있으며, 때로 렌더링 결과를 버릴 수 있음(프레임 드랍)

startTransition API를 사용하여 렌더링하는 동안 앱의 응답성을 유지하도록 선택할 수 있습니다.
즉, React는 렌더링을 일시 중지 할 수 있습니다. 이를 통해 브라우저는 그 사이의 이벤트를 처리할 수 있습니다.
이전 게시물의 startTransition API에 대한 자세한 내용을 확인하세요.
 
외부 저장소는 우리가 구독할 수 있는 것입니다.
외부 저장소의 예로는 Redux 저장소, Zustand 저장소, 전역 변수, 모듈 범위 변수, DOM 상태 등이 있습니다.

Internal stores

내부 저장소에는 props, context, useState, useReducer가 포함됩니다.

Tearing

Tearing은 시각적(UI) 불일치를 나타냅니다. UI가 동일한 상태에 대해 여러 형태를 나타냄을 의미합니다.
React 18 이전에는 이 문제가 발생하지 않았습니다.
그러나 React 18에서는 렌더링 중에 React가 일시 중지(suspend)됩니다.
즉 concurrent 렌더링이 이 문제를 유발할 수 있습니다.
이러한 일시 중지 사이에 업데이트는 렌더링에 사용되는 데이터와 관련된 변경 사항을 가져올 수 있습니다.
UI가 동일한 데이터에 대해 두 개의 다른 값을 표시하도록 합니다.
 
WG discussion of tearing <- 옆의 디스커션에서 논의된 내용을 살펴봅시다.
컴포넌트는 색상을 가져오기 위해 일부 외부 저장소에 액세스해야 합니다.
 
 
동기 렌더링을 사용하면 UI에서 렌더링되는 색상이 일관됩니다.
동기 렌더링은 티어링을 유발하지 않음
 
 

concurrent(동시) 렌더링에서 스토어의 데이터 'blue'를 이용해 처음에 렌더링한 색상은 파란색입니다.
렌더링 도중 React가 스토어를 업데이트하면 데이터는 'red'으로 업데이트됩니다.
React는 업데이트된 값 red를 활용하여 렌더링을 계속합니다.
이는 '티어링'으로 알려진 UI의 불일치를 유발합니다.

동시성 렌더링에 의한 티어링 발생

 

이 문제를 해결하기 위해 React 팀은
변경 가능한 외부 소스에서 데이터를 안전하고 효율적으로 읽을 수 있도록
useMutableSource 훅을 추가했습니다.
 
그러나 리액트 워킹 그룹(react Working Group)의 구성들은
기존 오픈소스 라이브러리의 구현에서
useMutableSource를 채택하기 어렵게 만드는 기존 API 컨트랙트의 결함(flaws with the existing API contract)을 보고했습니다.

기존 라이브러리들은 렌더링 시 ref를 이전 렌더링에서 사용한 데이터로 참조하는 패턴을 사용하고 있었지만,
리액트 코어 팀은 ref를 공유 데이터, 전역변수와 같이 불안전한 데이터로 생각하고 있었습니다.
그래서 렌더링 중간에 ref를 읽고 쓰는 것은 당연히 버그가 생기는게 맞다고 생각했었습니다.

  • 렌더링 중에 ref 값을 읽거나 쓰는 것은 지연 초기화 패턴(the lazy initialization pattern을 구현하는 경우에만 안전합니다.
  • ref는 다른 것과 마찬가지로 변경 가능한 소스(mutable source)이므로 다른 타입의 읽기는 안전하지 않습니다(unsafe).
  • 지연 초기화 패턴이 아닌 writing는 사실상 부작용(side effects)이 있기 때문에 안전하지 않습니다(unsafe).

이는 수 많은 기존 오픈소스 라이브러리 메인테이너들에과 사용자에게 어려움을 줄 것이었으므로,
많은 논의 끝에 useMutableSource 훅이 재설계되고 이름이 useSyncExternalStore로 변경되었습니다.
useSyncExternalStore는 이름처럼 동기적으로 렌더링하게 되었고,
이는 기존 오픈소스 라이브러리들에 큰 영향을 끼치지 않게 되었습니다.

 

Understanding useSyncExternalStore hook

React 18에서 사용할 수 있는 새로운 useSyncExternalStore 훅을 사용하면 저장소의 값을 적절하게 구독할 수 있습니다.
The new useSyncExternalStore hook
 

useMutableSource → useSyncExternalStore · Discussion #86 · reactwg/react-18

Since experimental useMutableSource API was added, we’ve made changes to our overall concurrent rendering model that have led us to reconsider its design. Members of this Working Group have also re...

github.com

마이그레이션을 단순화하기 위해 React는 새로운 패키지 use-sync-external-store를 제공합니다.

To help simplify the migration, React provides a new package use-sync-external-store

 

use-sync-external-store

Backwards compatible shim for React's useSyncExternalStore. Works with any React that supports hooks.. Latest version: 1.2.0, last published: 2 months ago. Start using use-sync-external-store in your project by running `npm i use-sync-external-store`. Ther

www.npmjs.com

이 패키지의 shim(/shim)는 어떤 리액트의 버전과도 잘 동작합니다.

import {useSyncExternalStore} from 'react';

// or

// Backwards compatible shim
import {useSyncExternalStore} from 'use-sync-external-store/shim';

//Basic usage. getSnapshot must return a cached/memoized result
useSyncExternalStore(
  subscribe: (callback) => Unsubscribe
  getSnapshot: () => State
) => State

// Selecting a specific field using an inline getSnapshot
const selectedField = useSyncExternalStore(store.subscribe, () => store.getSnapshot().selectedField);

useSyncExternalStore 훅은 두 개의 함수를 필요로 합니다.

  • 콜백 함수를 등록하는 'subscribe' 함수
  • 'getSnapshot'은 구독 값이 마지막 시간 이후 변경되었는지, 렌더링되었는지 확인하는 데 사용됩니다.
    • 문자열이나 숫자와 같은 변경할 수 없는 값이거나 캐시/메모된 객체여야 합니다.
    • 그런 다음 훅에서 변경할 수 없는 값을 반환합니다.

getSnapshot 결과를 자동으로 메모하는 API 버전입니다.

import {useSyncExternalStoreWithSelector} from 'use-sync-external-store/with-selector';

const selection = useSyncExternalStoreWithSelector(
  store.subscribe,
  store.getSnapshot,
  getServerSnapshot,
  selector,
  isEqual
);

 

Daishi Kato의 React 18 for External Store Libraries talk 강연에서 논의된 예를 확인해 보겠습니다.

import React, { useState, useEffect, useCallback, startTransition } from "react";

// library code

const createStore = (initialState) => {
  let state = initialState;
  const getState = () => state;
  const listeners = new Set();
  const setState = (fn) => {
    state = fn(state);
    listeners.forEach((l) => l());
  }
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  }
  return {getState, setState, subscribe}
}

const useStore = (store, selector) => {
  const [state, setState] = useState(() => selector(store.getState()));
  useEffect(() => {
    const callback = () => setState(selector(store.getState()));
    const unsubscribe = store.subscribe(callback);
    callback();
    return unsubscribe;
  }, [store, selector]);
  return state;
}

//Application code

const store = createStore({count: 0, text: 'hello'});

const Counter = () => {
  const count = useStore(store, useCallback((state) => state.count, []));
  const inc = () => {
    store.setState((prev) => ({...prev, count: prev.count + 1}))
  }
  return (
    <div>
      {count} <button onClick={inc}>+1</button>
    </div>
  );
}

const TextBox = () => {
  const text = useStore(store, useCallback((state) => state.text, []));
  const setText = (event) => {
    store.setState((prev) => ({...prev, text: event.target.value}))
  }
  return (
    <div>
      <input value={text} onChange={setText} className='full-width'/>
    </div>
  );
}

const App = () => {
  return(
    <div className='container'>
      <Counter />
      <Counter />
      <TextBox />
      <TextBox />
    </div>
  )
}
코드의 어딘가에서 startTransition을 사용하면 티어링이 발생합니다.
Tearing 문제를 해결하기 위해 이제 useSyncExternalStore API를 사용할 수 있습니다.
useEffect 및 useState 훅 대신 useSyncExternalStore를 사용하도록 라이브러리의 useStore 훅을 수정하겠습니다.
import { useSyncExternalStore } from 'react';

const useStore = (store, selector) => {
  return useSyncExternalStore(
    store.subscribe,
    useCallback(() => selector(store.getState(), [store, selector]))
  )
}

 

외부 저장소에서 useSyncExternalStore 훅으로 마이그레이션하는 것은 쉽고 잠재적인 문제를 피하기 위해 권장됩니다.

모든 인터페이스가 동일하기 때문에, 다른 라이브러리로 마이그레이션 하는것도 쉬울듯... 

어떤 라이브러리들이 concurrent rendering의 영향을 받을까요?

  • 렌더링하는 동안 변경 가능한 외부 데이터에 액세스하지 않고 React 프롭, 상태 또는 컨텍스트를 사용하여 정보만 전달하는 컴포넌트 및 사용자 정의 훅이 있는 라이브러리는 영향을 받지 않습니다.
  • 데이터 가져오기, 상태 관리 또는 스타일링(Redux, MobX, Relay)을 처리하는 라이브러리가 영향을 받습니다.
    • React 외부에 상태를 저장하기 때문입니다.
    • 동시 렌더링을 사용하면 React가 알지 못하는 사이에 이러한 외부 데이터 저장소를 렌더링 중에 업데이트할 수 있습니다.
useSyncExternalStore 훅에 대해 좀 더 자세히 알아보려면 아래 링크를 읽어보세요
반응형

'Web > Js' 카테고리의 다른 글

JS .js vs .mjs  (0) 2023.01.27
ES6 Map(), Set()  (0) 2023.01.19
useEffect와 useLayoutEffect  (0) 2023.01.11
React.memo와 useMemo 차이점  (0) 2023.01.11
[React]setState Callback 함수 사용  (0) 2023.01.11
반응형

React에는 굉장히 수많은 Hook이 있다. 지금도 계속 생기고 있을 것이다.

하지만 나의 경우에는 맨날 쓰는 Hook만 쓰긴 한다. useState  useEffect 정도… 사실 이것만 있어도 대부분의 기능을 구현하는데 문제 없었기에 그런 것도 있긴 하다만 React의 기능을 완전히 이용하지는 못하고 있었다.

오늘 회사에서 3분기 회고를 하는데 동료 분께서 이런 세태에 대해 언급하시면서 useLayoutEffect Hook이란게 있다라는 말씀을 하셨다. 뭔지는 몰랐지만 뭔지 궁금해져서 이번주에는 이에 대해 간단히 알아보도록 하자.

useEffect와 문제점

React Hook은 Functional Component에서도 기존 Class Component만이 제공하던 기능을 사용할 수 있도록 해준 기능이다.

그 중 핵심적인 기능이 상태 값 관리와 컴포넌트 생애 주기를 관리할 수 있다는 점이었는데, useEffect 는 그 중에서 생애주기에 집중하는 Hook이다.

기존의 Class Component에서 가지고 있던 몇 가지 메서드를 대체하는 역할을 한다. componentDidMount , componentDidUpdate , 그리고 componentWillUnmount 다. 컴포넌트의 Mount, 컴포넌트의 Update, 그리고 Unmount. 컴포넌트 생애 주기의 전부다.

기본적으로 useEffect 는 랜더링이 완료된 후에 동작한다. 이 랜더링에는 리랜더링도 포함된다. 혹은 특정 상태의 업데이트 시에만 구동되도록 할 수 있다.

과거에 썼던 글에서 사용한 코드 조각을 슬쩍 들고 왔다. 이걸로 useEffect 에 대해 살짝만 알아보자. 해당 글에서 useState  useEffect 에 대해 좀 더 자세히 알아볼 수 있다.

const App = () => {
  const [name, setName] = useState('');  useEffect(() => { // (1)
    console.log('render');
  });  useEffect(() => { // (2)
    setName('희찬');    return (() => { // (3)
      console.log('Cleanup'); 
    }
  }, []);  useEffect(() => { // (4)
    console.log(name);
  }, [name]);  return (
    <div>{name}</div>
  );
}

(1) useEffect 는 첫 랜더링과 매 리랜더링마다 동작할 것이다.

(2) 이 useEffect 는 Dependency Array가 [] 로 설정되어있다. 즉 비어있다. 이 경우에는 첫 랜더링에서만 동작할 것이다. 즉 componentDidMount 의 역할을 한다.

(3) return 안에 있는 메서드를 cleanup function이라고 한다. 이는 리랜더링 될 때 앞에 있는 callback을 실행하기 전, 혹은 Unmount할 때 동작한다. 정리하는 역할을 한다고 해서 cleanup이라고 하나보다. 이 경우에는 첫 랜더링에만 동작하는 useEffect 였으므로, 이 컴포넌트가 아예 Unmount될 때 동작할 것이다. 따라서 componentWillUnmount 의 역할을 할 수 있다.

(4) 이 경우에는 Deps에 [name] 이 들어있으므로, 리랜더링될 때 해당 Array 내의 값이 변경되어있으면 해당 메서드가 발생한다. 조건부 useEffect 라고 볼 수 있겠다.

 

그런데 useEffect 를 사용할 때 UI 입장에서는 문제점이 있다. useEffect  랜더링 후에 동작한다는 점이다.

문서를 보면 레이아웃 구성과 페인팅이 완료된 후에 발생한다고 한다. 이때 이벤트 리스너를 만들거나 비동기 작업을 시작하는 것은 적절하다.

하지만 만약 UI에 영향이 가는 작업이라면 어떨까? 일단 리랜더링이 다 된 이후에 useEffect 가 발동하면 초기화된 값과 우리가 원하는 값 사이의 괴리가 생길 수 있다.

function App() {
  const [name, setName] = useState<string>('');

  useEffect(() => {
    setName('heechan');
  }, []);
  
  return (
    <>
      <p>{`안녕하세요! ${name}`}</p>
    </>
  );
}

대충 이런 코드라고 치자.

랜더링 되자마자는 화면에 안녕하세요! 만 보이다, 찰나의 순간에 useEffect 발동 후 리랜더링되어 안녕하세요! heechan 이 된다. 그 번쩍이는 순간이 문제가 될 수 있다. (사실 보여주고 싶기도 했는데 생각보다 너무 랜더링이 빨리 돼서 눈에 안보인다… 더 복잡한 코드에서는 충분히 딜레이가 발생할 수 있다)

이런 사소한 문제들이 유저의 사용성을 해칠 수 있어서, 이런 문제를 해결할 방법이 필요하다.

useLayoutEffect

그래서 useLayoutEffect 가 등장했다. 위 캡처에서도 나와있듯 사용자에게 직접 UI로 보여야 하는 몇몇 요소의 경우에는 랜더링 이후로 지연할 수 없다. 번쩍거리는걸 보여주기보다는 그 이전에 동기화해야 한다.

이럴 때 useLayoutEffect 를 사용하면 된다. 사용법은 useEffect 와 완전 동일하다. useLayoutEffect(callback, [...]) 으로 쓰면 된다.

useLayoutEffect  레이아웃 구성과 페인팅 사이에 동기적으로 동작한다. useEffect 가 랜더링 후에 호출되는 것과 대조되는 특징이다.

이 이미지를 보면 그 순서를 확실히 알 수 있다. 업데이트 시 DOM 업데이트가 진행된 후에 useLayoutEffect 에 있는걸 실행한다. 그 후 브라우저에서 페인팅을 마치면 그 후에서야 useEffect 에 있는 callback을 실행한다.

이렇게 되면 브라우저가 페인팅, 즉 유저에게 보이기 전에 우리가 필요한 작업을 하기 때문에 번쩍이는 현상 없이 잘 보이게 된다.

그러면 아까 위에 적었던 코드도 이렇게 바꾸면 문제 없이 동작한다는 것이다.

function App() {
  const [name, setName] = useState<string>('');

  useLayoutEffect(() => {
    setName('heechan');
  }, []);
  
  return (
    <>
      <p>{`안녕하세요! ${name}`}</p>
    </>
  );
}
 

근데 위의 코드를 다시 유심히 살펴보자. 난 처음에 이 코드를 보고 이게 왜 동작하는건지 상당한 의구심이 생겼다.

분명히 useState 가 제공하는 setter의 경우 (위에서는 setName ), 컴포넌트의 리랜더링을 유도하는 것으로 알고 있는데, LayoutEffect가 랜더링 과정 중에 돌아간다고 해서 바로 바뀌는건 아니지 않나? 싶었다.

나의 상상 속에서는 DOM 업데이트 -> Layout Effect 실행 -> 여기 setName 이 있네? 나중에 리랜더링이 필요해! -> 페인팅 -> 리랜더링 -> 우리가 원하는 값으로 페인팅 이 과정이었다.

그래서 궁금해서 찾아보니 React 콜라보레이터 한 분이 남긴 Github Issue 댓글을 확인할 수 있었다.

https://github.com/facebook/react/issues/17334#issuecomment-553984285

LayoutEffect가 리랜더링을 필요로 할경우, 이 리랜더링 과정도 동기적으로 진행되며 브라우저의 페인팅이 지연된다는 말이었다.

그래서 useLayoutEffect 내에서 useState 의 setter를 사용하더라도 리랜더링이 나중에 진행되는게 아니라 바로 동기적으로 해야 할 리랜더링 다 진행 후 페인팅하기 때문에 번쩍거림, 쓸데 없는 페인팅에 대한 걱정을 덜 수 있다.

 

또 한 가지 궁금해진 점인데, 과거에 useMemo 도 랜더링 타임에 진행된다고 한 적이 있다. 그러면 useMemo , useLayoutEffect 중에 뭐가 먼저 동작할까?

 

이렇게 해서 첫 랜더링 때 flag 값을 바꾸면, 그 이후 무엇이 주르륵 실행될지 확인해보았다.

이 순서대로 동작했다. 혹시 순서 때문에 그런가 싶어 순서도 바꿔보았는데 그대로 이렇게 나온다.

아마 useMemo 가 호출되는 타이밍은, DOM 랜더링 과정 중이 아닐까 싶다. (근거를 못찾아서 확신은 없음) useLayoutEffect 는 그 이후, 페인팅이 되기 직전에 불리고, useEffect 는 페인팅까지 다 되고 나서 불리니까 저 순서가 맞지 않을까 싶다.

언제 무엇을 써야 하는가

위에서 계속 언급한 컴포넌트가 번쩍 거릴 수 있는 문제를 해결할 때 useLayoutEffect 를 사용하는 것이 도움이 될 것이다.

다만 최대한 useEffect 만으로 할 수 있는 방법을 고안해보고 사용하라고 한다. 기본적으로 useEffect 가 랜더링 후에 동작하는 이유는 랜더링 과정을 방해하지 않게, 오래걸리지 않게 하기 위해서다. useLayoutEffect 를 너무 남용하면 그런 의미를 퇴색시킬 수 있으니 꼭 필요할 때만 쓰라는, 그런 의미가 아닐까 한다.

 

React 18부터는 useEffect 의 호출 타이밍도 경우에 따라 달라질 수 있다고 한다.

뭔가 업데이트하는 로직이 flushSync 문 안에 들어있거나, 아니면 Discrete Event가 발생했을 때 useEffect 의 callback이 랜더링 전에 동기적으로 불린다는 내용이다.

useLayoutEffect 와는 다른게, useLayoutEffect 는 내부에서 발생하는 업데이트까지 전부 수행한 후 페인팅하지만, 이 useEffect 는 그냥 useEffect 내부에 있는 callback만 빠르게 호출하는거지 그 내부에서 발생하는 업데이트 요청은 페인팅 이후에 진행된다. 따라서 약간 용법이 다르다고 볼 수 있겠다.

이 새로운 useEffect  UI 업데이트보다는, 그 전에 불린 callback이 끝났는지 확인해야 할 때 사용하도록 유도되는 것으로 보인다.

이걸 보면 유저가 제출 버튼을 따닥 눌러서 여러번 실행될 수 있을 때, 그 전 작업이 종료되었는지를 확인해야 할 때, 동기적으로 동작하도록 유도할 수 있다.

사실 Discrete Event에 대해서는 잘 모르긴 하지만, 한 이벤트가 다음 이벤트에 영향을 줄 수 있는 이벤트를 의미한다고 한다. Click, Press 이런 이벤트들이 있다고 한다.

결론

사실 프레임워크보다는 웹 표준적인 근본 공부를 좀 더 해야 하긴 하는데…

그래도 useLayoutEffect 는 굉장히 흥미롭다. 적절히 잘 쓰면 우리 회사 프로덕트에도 큰 도움이 되지 않을까 싶기도 하다.

참고한 것

 

반응형

+ Recent posts