프론트/javascript

클로저 패턴이란 무엇인가

ZestLee 2024. 4. 20. 15:46

들어가며

의도치 않게 너무 어그로 끄는 제목이 되었는데, 별 생각 없이 정한 제목입니다...

이 글은 그저 끄적이는 글이기 때문에 엄청난 의식의 흐름일 수 있습니다

갑자기 뿅 하고 끝날 수도 있으니, 이 점 양해 부탁드립니다 ㅎ;;

 

배경

최근 react 를 공부하면서, 클로저 패턴을 자주 쓰게 되었음

(예를 들어, click과 같은 이벤트 핸들러 함수에 특정 값을 넘기면서 이벤트도 함께 넘기고 싶을 때 이 패턴을 활용하곤 하더라)

 

예시

export default function App() {
  const togglePopover = (index) => (e) => {
    e.preventDefault();
    e.stopPropagation();
    setOpen(openedIndex === index ? null : index);
  };
  
  return (
    <div className="wrapper">
      {dummyData.map(({ text, context }, i) => (
        <Detail
          key={`detail${i}`}
          text={text}
          context={context}
          open={openedIndex === i}
          onToggle={togglePopover(i)}
        />
      ))}
    </div>
  );
}

// 생략된 부분 다수...

 

  • 위에서 `togglePopover` 는 클로저 함수임
    • index 를 매개변수로 받고, 새로운 실행 컨텍스트를 생성하고, 함수를 반환함
    • 반환된 함수는 이벤트 객체를 매개변수로 받고, 그 이벤트 객체를 사용해서 특정 작업을 수행함
    • 이 반환된 함수는 원래 받았던 index 에 계속해서 접근이 가능함
    • 이것은 클로저로 index 가 함수 내부에서 기억되기 때문임
  • 그렇다면 굳이 왜 클로저 함수를 사용할까?
    • 성능 최적화와도 관련이 있다고 함
    • 만약 togglePopover 함수를 클로저 함수가 아닌, index 와 e 두개의 인자를 받는 함수로 만든다면
      onToggle={(e) => togglePopover(i, e)} 이런 식으로 호출해야 함
      이렇게 되면 onToggle 이벤트가 발생할 때마다 새로운 (e) => togglePopover(i, e) 함수가 생성이 됨
    • 하지만 클로저 패턴으로 하면 특정 i 값에 대한 함수가 한 번만 생성이 됨
      이 함수는 i 값을 기억하고, i 에 대한 이벤트가 발생할 때 재사용 됨
  • 이게 왜 중요하냐면 리액트는 컴포넌트의 prop 이나 state 가 변경될 때 다시 렌더링 함
    • 이벤트 핸들러와 같은 함수가 새로운 참조를 가지게 되면,
      리액트는 이것을 변경된 prop 으로 간주하고,
      해당 컴포넌트를 불필요하게 다시 렌더링 하게 되는 것임

그렇다면 클로저가 뭔데?

드디어 다시 돌아오게 되었음..;;

 

클로저는 함수랑 함수가 선언될 때의 렉시컬 환경 (어휘적 환경) 에 대한 조합임

렉시컬 환경은 해당 함수가 생성될 때의 변수들을 포함하고 있으며, 이를 통해 함수가 실행될 때도 그 변수들에 접근할 수 있게 됨

주로 함수형 프로그래밍에서 사용되고, 함수 내부에서 생성된 내부 함수가, 외부 함수의 지역 변수에 접근할 수 있게 해 줌

클로저를 통해서 데이터를 은닉하고 모듈 패턴과 같은 구조에서 특정 데이터에 대한 접근을 제어할 수 있음

 

데이터를 은닉하면 좋은 점이 뭔데?

(옛날에) 학교에서도 배웠는데... 맨날 까먹는

  1. 유지보수가 용이해짐
    1. 직접 접근하고 수정하는 걸 제한해서 복잡성이 줄어듦
    2. 데이터를 변경하거나 로직을 수정할 때 다른 코드에 미치는 영향을 최소화 할 수 있게 됨
  2. 충돌 감소
    1. 여러 개발자가 동일한 코드 베이스에서 작업할 때, 변수 or 함수 or 네임스페이스 충돌을 방지해 줌
    2. 각 데이터와 함수는 인터페이스 뒤에 숨겨져있기 떄문임
  3. 보안이 향상됨
    1. 중요한 데이터를 외부에서 직접 접근할 수 없도록 함. 따라서 데이터의 무단 변경이 되지 않음
  4. 인터페이스와 구현의 분리
    1. 개발자는 객체의 인터페이스만을 통해서 상호작용을 하게 됨
    2. 따라서 내부 구현이 변경되더라도, 인터페이스가 동일하다면 타 개발자 측을 변경할 필요가 없게 됨
    3. 이것은 확장성을 개선할 수가 있다
  5. 재사용성 향상
    1. 외부에서 필요한 기능만을 노출시키고 내부 구현을 숨길 수가 있게 됨
    2. 이렇게 되면 코드나 컴포넌트를 재사용할 수 있게 됨

클로저와 클래스는 뭐가 다른건데?

내 입장에서는 클로저와 클래스가 데이터를 은닉한다는 점에서 넘 비슷하단 생각이 들었음

((((((((((나)))))))))

 

그래서 생각난 김에 정리해 보려고 함

예시

계산기로 예시를 만들어 보자

- 클래스

class Calculator {
  constructor() {
    this.currentValue = 0;
  }

  add(value) {
    this.currentValue += value;
    return this.currentValue;
  }

  subtract(value) {
    this.currentValue -= value;
    return this.currentValue;
  }

  getCurrentValue() {
    return this.currentValue;
  }
}

const calc = new Calculator();
console.log(calc.add(10)); // 10
console.log(calc.subtract(4)); // 6
console.log(calc.getCurrentValue()); // 6
  • new 해서 새로운 인스턴스를 만들 수 있고, 각 인스턴스는 자신만의 상태를 유지함
  • OOP 의 일환으로, 구조화된 방식을 제공함
  • 상속과 같은 기능으로 복잡한 시스템을 구축할 때 유용함
function Calculator() {
  let currentValue = 0;

  return {
    add: function(value) {
      currentValue += value;
      return currentValue;
    },
    subtract: function(value) {
      currentValue -= value;
      return currentValue;
    },
    getCurrentValue: function() {
      return currentValue;
    }
  };
}

const calc = Calculator();
console.log(calc.add(5)); // 5
console.log(calc.subtract(2)); // 3
console.log(calc.getCurrentValue()); // 3
  • 이것도 새로운 인스턴스를 갖게 됨
  • 함수의 스코프를 이용해서 데이터 숨김에 초점을 둠
  • 메모리 최적화가 필요하거나 함수형 프로그래밍 패턴을 선호할 때 유용함