보통의 함수는 다음과 같이 사용한다.

const greet = (greeting, name) => {
  console.log(`${greeting} ${name}`);
}

greet('Hi', 'Tony');
greet('Hi', 'John');
greet('Hi', 'Tina');

 

함수를 연속해서 반환하는 커링(Currying)를 사용하면 다음과 같은 것이 가능해진다.

const curry = function(greeting) {
  return function(name) {
    console.log(`${greeting} ${name}`);
  }
}

const curriedGreet = curry('Hi');

curriedGreet('Tony');
curriedGreet('John');
curriedGreet('Tina');

이는 렉시컬 환경과 클로저에 의해 가능하다.

  1. const curriedGreet = curry('Hi'); 을 통해서 변수가 ‘Hi’로 정의된 렉시컬 환경을 갖는 클로저가 생성된다.
  2. curredGreet(’…’); 은 기존에 생성된 curredGreet의 클로저를 통해 실행된다.
    • greeting 변수와 name 변수가 사용되는데, greeting 변수는 curry(’Hi’) 에서 생성된 렉시컬 환경에서, name 변수는 curredGreet()을 호출하며 사용되는 파라미터로 사용된다.

 

위의 코드는 아래와 같이 간결하게 작성할 수 있다.

const curry = greeting => name => {
  console.log(`${greeting} ${name}`);
};

여기서 데이터도 변경해 보고, 벤치마크 직접 돌려볼 수 있다.

내가 넣은 데이터는 요녀석이었다.

{a: "hello", c: "test", po: 33, arr: [1, 2, 3, [4, [5, 6]]], anotherObj: {a: [1, 2, 3, [4, [5, 6]]], str: "whazzup"}};
 

JSBEN.CH Performance Benchmarking Playground for JavaScript

 

jsben.ch

 

결과를 그냥 말해주자면 아래의 사진과 같다.

Object.assign이 가장 빠르며, 유의미하게 lodash의 _.cloneDeep이 가장 느리다.

 

내가 구현한 커스텀 깊은 복사 메서드는 아래와 같다. 사이사이에 주석을 잘 달아놨으니, 읽어보면 이해가 잘 갈 것이다.

그래도 간단히 설명을 덧붙이자면 재귀적으로 구현했다. 배열이거나 객체면 깊게 들어가서 잘 반환해 주는 식이다.

function cloneDeep(obj) {
  if (typeof obj !== 'object' || obj === null) {
    // 객체가 아니거나 null인 경우 그대로 반환
    return obj;
  }

  if (Array.isArray(obj)) {
    // 배열인 경우 빈 배열 생성 후 요소들을 재귀적으로 복사
    return obj.map(item => cloneDeep(item));
  } else {
    // 객체인 경우 빈 객체 생성 후 속성들을 재귀적으로 복사
    const clonedObj = {};
    for (const key in obj) {
      clonedObj[key] = cloneDeep(obj[key]);
    }
    return clonedObj;
  }
}

lodash가 워낙에 무겁다고 해서 직접 구현도 해보고, 비교도 해봤는데 진짜 차이가 꽤 크긴 하다.

물론 내 구현 방식을 lodash의 deepClone과 비교해 봤을 때 빠진 부분이 분명히 많겠지만(정규식이라던가), 그래도 내가 만든 재귀적인 함수에서도 웬만한 데이터에서의 깊은 복사는 다 할 수 있을 것으로 예상이 된다.

JSON 객체를 이용한 깊은복사 역시 number, string, boolean, object, array, null 에서만 복사가 가능하다. 이를 생각해봤을 때 내가 만든 재귀와 비교해볼만한 메서드는 stringify + parse이며, 직접 구현해서 사용하면 훨씬 편하다는 것을 알 수 있다.

Object.assign은 얕은 복사니까 굳이 신경 쓰진 않겠다.. 그냥 얼마나 빠른지만 궁금했다.

 

결론

1. 귀찮아도 JSON으로 깊은 복사는 지양하자.

 

2. 재귀는 아직 내겐 '감'으로 짜는 영역인듯하다. mock 데이터 하나 머리로 떠올리고 함수 하나하나 내부로 들어가서 상상으로 돌려보면 골치 아프다. 최소한 펜이랑 종이는 있어야 할 듯

어쩌다보니..

혼자 공부할 때 useEffect 언마운트에서 가볍게 스냅샷으로 작동하는 것 같다며 마지막에 언급하고 끝났었다.

그러나 스냅샷보다 훨씬 중요한 무언가가 있었다..

 

바로 클로저였다.

퀴즈

let count = 1;
function makeCounter() {
  count = 0;

  return function() {
    return ++count;
  };
}
count = 10;

let counter = makeCounter();
console.log(counter()); // <-- 뭘까??

정답을 모른다면(글을 읽고 있는 다른 사람이든, 미래의 나든) 밑으로 쭉 내려서 자바스크립트 공식문서를 읽는 편이 좋을듯 하다. 클로저 개념은 필수다!!

일단 퀴즈의 정답은 1이다.

 

나도 오늘 막 배웠는데, useEffect의 언마운트에서 왜 기묘한 일이 발생하는지 이제야 이해할 수 있었다.

 

이전의 포스팅은 나만 볼 수 있으니, 여기에 좀 복붙을 해오자면,

 

특이한 점

import { useEffect, useState } from "react";
const StarRating = ({starCount}) => {
  const [starClickedNum, setStarClickedNum] = useState(0);
  const stars = [];

  // !! 여기부터 중요 !!
	console.log(starClickedNum);
	useEffect(() => {
	  const currentTimetoString = new Date().toLocaleString();
	  return () => {
	    console.log(starClickedNum);
	    console.log(currentTimetoString);
	    console.log(new Date().toLocaleString());
	  }
	}, [starClickedNum])
  // !! 여기까지 중요 !!

  for (let i = 0; i < starCount; i++) {
    stars.push(<img src="/assets/blueStar.png" alt="star" className="w-4 h-4" />);
  }

  return (
    <div className="flex">
    {stars.map((star, index) => (
      <div onClick={() =>setStarClickedNum(prev => prev + 1)} key={index}>{star}</div>
    ))}
    </div>
  )
}

export default StarRating;

starClickedNum이 바뀌었을 때 첫 번째 줄의 starClickedNum은 업데이트 된 값으로,

클린업 함수 내부의 starClickedNum은 업데이트 이전의 값으로 출력된다.

이런식으로 오히려 값이 다시 작아진다.

useEffect 내부만의 스냅샷이 아닌, 전체 코드의 스냅샷을 기준으로 하는듯 하다.

참고로, new Date()의 경우는 실제 실행 시간을 기준으로 출력된다.

결과적으로는 일단 위에서부터 출력하고, dependency 배열 내부의 값이 바뀌기 이전의 스냅샷을 기반으로 클린업 함수를 실행한다.

이렇게 대충 혼자 이해하고 넘어갔었다.

 

결론..

내가 이해한 내용이 대충은 맞다. 그래도 원리는 제대로 모르고 억지로 이해했으니 클로저와 렉시컬 환경을 아는 시점에서 다시 작성해보겠다.

 

예시로 나와있는 클린업 함수 내부에서 starClickedNum을 찾는 과정은 이렇다.

return에 있는 익명함수 → useEffect 내부 → 컴포넌트 전체 (useState에 의해 정의된 starClickedNum이 있음(찾음)) → !! 출력 !!

위의 모든 과정은 리랜더링 이전에 있던 렉시컬 환경에서 이뤄진다.

 

따라서 리랜더링되며 출력된 starClickedNum엔 새로운 렉시컬 환경의 업데이트 된 값이,

클린업 함수 내부에서 출력되는 값은 starClickedNum이 업데이트 되기 전 렉시컬 환경의 값이 들어가 있는 것이다..

 

클린업 함수도 결국엔 함수니까, 숨김 프로퍼티 [[Environment]] 가 있고, 이를 사용해서 자신이 만들어진 환경으로 간다. 그리고 변수에 접근한다.

+

new Date().toLocaleString()은 변수가 아니니까 굳이 렉시컬 환경에서 어떤 값이었는지 알 필요가 없다. 그냥 함수를 실행시켜서 클린업 함수 실행 시점의 시간을 출력하기만 하면 된다.

 

가렵던 부분을 긁은 느낌이다.

참고

https://ko.javascript.info/closure

 

변수의 유효범위와 클로저

 

ko.javascript.info

 

intersection observer 이해하기 에서 이어지는 내용이다.

사진에서 보이듯이, 배열에 원소가 하나 들어갔다가 2개 들어갔다가.. 아주 난리다.

 

여러 대상을 관찰하는 경우에도 보통 entries 배열에는 하나의 요소만 들어있는 것이 정상이다.

 

이 동작의 이유는 !!!!!!!

관찰 + 교차되는 대상에 대해 IntersectionObserver 객체의 콜백 함수가 실행되기 때문이다!!!!!!

코드에서 각각 다른 대상 요소를 관찰하는 3개의 개별 IntersectionObserver 인스턴스를 사용하고 있다. 따라서 콜백 함수는 교차된 각 대상에 대해 한 번씩 호출된다.

결론: 콜백 함수는 관찰된 각 대상에 대해 개별적으로 호출되므로 entries에는 observer.observe를 통해서 구독해놓은 녀석들이 전부 보이는 것이 아니다. 그 중 교차되고 있는 녀석들만 확인할 수 있는 방식이다.

 

여기서 직접 코드를 굴려볼 수 있다.

 

Edit fiddle - JSFiddle - Code Playground

 

jsfiddle.net

 

+ Recent posts