선요약 - 클로저를 이용한 쓰로틀링 util함수

const numberElem = document.querySelector(".number");
const buttonElem = document.querySelector(".add-button");

function add(elem) {
  elem.innerText++;
}

function createThrottle() {
  const timeoutRef = { id: null };
  const throttle = (fn, timeout = 500) => {
    if (timeoutRef.id) {
      return;
    }
    fn();
    timeoutRef.id = setTimeout(() => {
      timeoutRef.id = null;
    }, timeout);
  };

  return throttle;
}

const throttle = createThrottle();

function handleFirstNumberClick() {
  throttle(() => add(numberElem));
}

function initFirstNumberClickEvent() {
  buttonElem.addEventListener("click", handleFirstNumberClick);
}

initFirstNumberClickEvent();

 


 

클로저를 이용해 쓰로틀링 util함수화 하기


1. 기초적인 방식의 쓰로틀링

count를 올리는 클릭이벤트에 대해 쓰로틀링을 걸어보자

 

 

클릭을 연달아 빠르게 해도 쓰로틀링이 걸려 count는 클릭 수 보다 적게 올라간다.

잘 작동하지만 로직이 한군데에 뭉쳐있어 어지럽다. 로직을 분리해 보자.

 

2.  로직을 분리한 쓰로틀링과 문제점

function add() {
  numberElem.innerText++;
}

let timeoutID;

function throttle(fn, time) {
  if (timeoutID) return;
  timeoutID = setTimeout(() => {
    fn();
    timeoutID = null;
  }, time);
}

function handleClick() {
  throttle(add, 500);
}

buttonElem.addEventListener("click", handleClick);

 

보기좋게 나누었고 얼핏 throttle 이란 함수도 재사용할 수 있어 보이지만 전역변수 timeoutID 에서 기인하는 몇 가지 문제가 있다.

  • 전역공간을 더럽힌다는 것 자체로 한가지 문제
  • throttle함수를 재사용하게 된다면 여러 이벤트에서 timeoutID를 공유하게 되어 잠재적으로 문제발생

 

문제상황

 

두번째 이벤트가 작동하지않는다. 두번째 이벤트가 발생하기 이전에 첫번째 이벤트에 의해 타임아웃ID가 부여되기 때문이다. 이를 해결하기 위해 전역번수 timeoutID를 캡슐화 해보자

 

3. 캡슐화

const firstNumberElem = document.querySelector(".number");
const secondNumberElem = document.querySelector(".second-number");
const buttonElem = document.querySelector(".add-button");

function add(elem) {
  elem.innerText++;
}

function throttle(fn, timeoutRef, timeout = 500) {
  if (timeoutRef.id) {
    return;
  }
  timeoutRef.id = setTimeout(() => {
    fn();
    timeoutRef.id = null;
  }, timeout);
}

function handleFirstNumberClick(timeoutRef) {
  throttle(() => add(firstNumberElem), timeoutRef);
}

function handleSecondNumberClick(timeoutRef) {
  throttle(() => add(secondNumberElem), timeoutRef);
}

function initFirstNumberClickEvent() {
  let timeoutRef = { id: null }; // 원시값을 사용해선 안됨
  buttonElem.addEventListener("click", () => handleFirstNumberClick(timeoutRef));
}
function initSecondNumberClickEvent() {
  let timeoutRef = { id: null };
  buttonElem.addEventListener("click", () => handleSecondNumberClick(timeoutRef));
}

initFirstNumberClickEvent();
initSecondNumberClickEvent();

 

전역 변수를 없앴고 잘 작동한다. timetoutRef 자리에 이전처럼 timeoutID로 원시값(null)을 넣게되면 이벤트핸들러의 인자로 값 자체가 들어가기 때문에 항상 id는 항상 null이 될 것이고 정상작동하지 않을것이다. 그를방지하기 위해 객체를 만들어 참조주소를 이벤트핸들러의 인자로 전달한다. 하지만 아직도 문제는 존재한다.

  • throttle이라는 함수를 사용 할 때마다 객체를 수동으로 하나씩 만들어야함
  • 함수가 동작하기 위해서 외부요인이 필요하기 때문에 유틸함수로 사용하기에 애매하고 불편

클로저를 이용해 이를 해결해 보자.

 

4. 클로저를 이용한 캡슐화 및 재사용성 확보

 

전역변수 캡슐화에 성공했고 재사용가능한 형태의 throttle 함수도 만들어졌다.

 


예전에 만들어두었던 웹 포트폴리오의 풀스크린 스크롤 기능을 리팩토링 하려다 이렇게까지 오게됐다....
lodash를 사용했다면 고민거리도 없었겠지만 그래도 클로저도 나름 실용적으로 써보고 좋은 공부가 된 듯하다.

1. 배열 구조분해할당

1 - 1 인덱스 호출 피하기

const userDataArr = ["kim", 20];

배열의 0번 인덱스에는 유저의 이름을 1번 인덱스에는 나이를 가지고 있는 데이터가 있다고 가정하자.

 

//bad
doSomething(userDataArr[0]);
doSomething(userDataArr[1]);

타인이 이 코드를 본다면 userDataArr[0], userDataArr[1] 도통 무슨 데이터 인지 알 수 없다. 직접 userDataArr를 확인해야만 알 수 있으므로 매우 번거롭다.

 

1 - 1 해결책 - 1: .. bad

const name = userDateArr[0]
const age = userDateArr[1]
doSomething(name);
doSomething(age);

값을 인덱스로 호출해 데이터의 정체를 알 수 없게 되는 문제는 해결했지만 문제는 아직 남아있다.

  • 만일 인덱스가 1 이 끝이 아니라면?
const name = userDateArr[0]
const age = userDateArr[1]
const address = userDateArr[2]
const score = userDateArr[3]
/// ...

작성하기 상당히 고통스러울 것이다

 

1 - 1 해결책 - 2: 구조분해할당

const [name, age] = userDataArr;
doSomething(name);
doSomething(age);
  • good

 

1 - 1 인덱스 호출 피하기 응용 - 함수파라미터

// bad
const consoleUserData = (userDataArr) => {
  console.log(`hi I'm ${userDataArr[0]}(${userDataArr[1]})`);
};

// good
const consoleUserData = ([name, age]) => {
  console.log(`hi I'm ${name}(${age})`);
};

1 - 2 변수교환: 고전적인 방법

let a = "aaa";
let b = "bbb";
  • a"bbb"b"aaa"로 바꾸고 싶은상황

 

1 - 2 변수교환 해결책 1 ..soso

let temp = a;
a = b;
b = temp;
  • 고전적인 방법
  • 변수를 추가적으로 만들어야하는게 단점

 

1 - 3 변수교환 해결책 - 2: 구조분해할당

[b, a] = [a, b]; //  이해가 안간다면 이걸보자. [b, a] = ["aaa", "bbb"]

처음 봤을 땐 이상해 보였지만 익숙해진다면 직관적이면서 고전적인 방법보다 코드도 짧고 변수도 적다는 장점이있다.

 

 

2. 객체 구조분해할당

2 - 1 DRY하게 작성하기

const userDataArr2 = { private: { age: 20, address: "somewhere" }, public: { name: "kim" } };
  • 위와 같은 객체가 있고 address와 name이 필요하다고 가정
//bad
doSomeThing(userDataArr2.private.address);
doSomeThing(userDataArr2.public.name);

물론 경우에 따라 상위 객체를 표시하는게 가독성이 좋을 수도 있지만 그렇지 않다고 가정하자. 데이터가 필요할 때마다 userDataArr2.xxx.ttt.zzz..... 하는 것은 상당히 고통스러울 것이다.

2 - 1 DRY하게 작성하기 - 해결책: 구조 분해 할당

//good
const { private: { age, address }} = userDataArr2;
doSomeThing(address);
doSomeThing(age);
  • good

2 - 1 DRY하게 작성하기 - 응용: 함수 파라미터

///bad
const showUserAddress = (userDataArr2) => {
doSomeThing(userDataArr2.private.address)
console.log(userDataArr2.private.address)
}

//good
const consoleUserAddress = ({private:{address}}) => {
doSomeThing(address)
console.log(address)
}

forEach 메소드를 break 시키기

  • 성능최적화를 위해 forEach를 돌다 break 시키고 싶을 때가 있다.
  • but forEach를 break 하는 것은 지원되지 않는다. 사용시 에러뜸

방법1 try catch 문

const arr = ["a", "b", "c", "d"];

// c 에서 멈추고 싶은 상황
try {
  arr.forEach((elem) => {
    console.log(elem);
    if (elem === "c") throw Error;
  });
} catch {}

//출력
// a
// b
// c
  • catch 해서 처리할 작업이 있다면 나쁘지않은 선택

방법2 for of 문

const arr = ["a", "b", "c", "d"];

// c 에서 멈추고 싶은 상황

for (let elem of arr) {
  console.log(elem);
  if (elem === "c") break;
}

//출력
// a
// b
// c
  • catch 해서 처리할게 없다면 for of 쓰는게 좋을듯하다.(코드 깔끔)

for of 문에서 index를 사용해야 할 때 (entries 활용)

const arr = ["a", "b", "c", "d"];

// c 에서 멈추고 싶은 상황

for (let [idx, elem] of arr.entries()) {
  console.log(idx, elem);
  if (elem === "c") break;
}

//출력
// 0 a
// 1 b
// 2 c

Date 표준내장객체

  • Date 객체는 특성 시점의 시간과 그 시간을 다루 수 있는 다양한 메소드를 가지고있다.

기초

//인자 없이 사용시 현재기기의 현재시간을 담은 인스턴스생성
const myTime = new Date();
/*
Tue Nov 08 2022 17:22:54 GMT+0900 (한국 표준시)
getDate()
...
... 메소드들
*/
//인스턴스 생성 1
// 날짜 형식을 담은 string을 인자로 받음. 후에 형식 변환 메소드를 위해 유용하게 사용가능

new Date("2022-11-26T13:30:00"); // Tue Nov 08 2022 17:22:54 GMT+0900 (한국 표준시)
new Date("2022-11-26"); // 시간이 생략되면 자동으로 09:00:00으로 설정됨.
new Date("2022-11"); // 일이 생략되면 자동으로 1일로 설정됨.
new Date("2022"); // 월이 생략되면 자동으로 1월로 설정됨.
//인스턴스 생성 2
// milliseconds 를 인자로 받음

//Date.now() 는 Date 의 static 메소드로 현재 시간을 밀리세컨드로 나타냄
const mili = Date.now(); // 1667897182224

new Date(mili); // Tue Nov 08 2022 17:46:22 GMT+0900 (한국 표준시)
//인스턴스 생성3
// new Date(년, 월[, 일, 시, 분, 초, 밀리초])
//일 생략 - 1일, 시생략 0시
// 월은 0부터 1월..
new Date(2022, 0, 15); // Sat Jan 15 2022 00:00:00 GMT+0900 (한국 표준시)

활용 - 형식변환

  • api 등에서 받아온 다양한 형식의 날짜 데이터를 내장 date 메소드를 이용해 가공할 수 있다.
const dateFromApi = "2022-11-08T08:57:16.975Z"; // ISO 형식

const originDateObj = new Date(dateFromApi); // date객체로 만들어서 메소드활용

originDateObj.toLocaleString("kr-KR"); // '2022. 11. 8. 오후 5:56:46'
originDateObj.toLocaleString(""); //생략시 기기의 국가 설정. '2022. 11. 8. 오후 5:56:46'

// options 참고 - mdn

const optionsA = {
  month: "long", // 영어에서만 long, short, 의미(mar, march)
  day: "numeric",
  weekday: "long",
};

const optionsB = {
  month: "long",
  day: "2-digit",
  weekday: "shor",
};

const optionsC = {
  year: "numeric",
  month: "long",
  day: "2-digit",
};

const optionsD = {
  year: "2-digit",
  month: "long",
};

originDateObj.toLocaleString("kr-KR", optionsA); // 1월 1일 토요일
originDateObj.toLocaleString("kr-KR", optionsB); // 11월 08일 (화)
originDateObj.toLocaleString("kr-KR", optionsC); // 2022년 11월 08일
originDateObj.toLocaleString("kr-KR", optionsD); // 22년 11월

ref

클로저

function makeAdder(x) {
  let y = 10;
  return function (z) {
    // 익명함수
    return x + y + z;
  };
}
  • makeAdder를 실행하면 익명함수를 반환한다.
  • makeAdder 가 실행 될 때 익명함수의 입장에서 외부 변수인 x, y를 기억하게 된다.
const fn = makeAdder(3);
/*
fn = (z) => 3 + 10 + z
*/

fn(2); // 3 + 10 + 2 = 15
  • 단순히 변수 x, y를 기억하고 사용하는 것으로 보이지만 사실은 그렇지 않다.
  • fnmakeAdder가 실행 될 때 부터 고유의 lexical 환경을 보유하게 된다.
  • 그리고 그러한 렉시컬 환경에 있는 변수 x, y를 통제할 수 있으며 위의 예시에서는 단순히 값을 가져온것일 뿐

이해를 돕기위한 예시

function useFoo() {
  let foo = 0;

  function getFoo() {
    return foo;
  }

  function setFoo(value) {
    foo = value;
  }

  return [getFoo, setFoo];
}

const [getFoo, setFoo] = useFoo(); // 환경 A 생성
const [getFoo2, setFoo2] = useFoo(); // 환경 B 생성

getFoo(); // 0

setFoo(5);

getFoo(); // 5

getFoo2(); // 0
  • useFoo 가 실행되었으니 getFoosetFoo가 공유하는 A 라는 고유의 렉시컬 환경이 생성되었다.
  • useFoo를 다시 한번 실행하면 또다른 환경 B가 생성된다.
  • setFoo(5)를 통해 A환경의 변수 foo는 5가 되었다.
  • getFoo2setFoo2의 환경인 B에서 foo는 여전히 0 이다.
  • 만약 foo가 const 로 선언되었으면 setFoo 는 작동하지않고 에러가 날 것이다.(setFoo는 foo값을 바꾸기 때문)

 

그래서 클로저란?

 

  • 빨간색 영역은 리턴될 함수가 갖는 렉시컬환경을 시각화한것..
  • fn과 같이 고유의 렉시컬 환경을 가진 함수를 클로저라한다.

+ Recent posts