반응형
Jake Seo
제이크서 위키 블로그
Jake Seo
전체 방문자
오늘
어제
  • 분류 전체보기 (715)
    • 일상, 일기 (0)
    • 백준 문제풀이 (1)
    • 릿코드 문제풀이 (2)
    • 알고리즘 이론 (10)
      • 기본 이론 (2)
      • 배열과 문자열 (8)
    • 데이터베이스 (15)
      • Planet Scale (1)
      • MSSQL (9)
      • 디비 기본 개념 (1)
      • SQLite 직접 만들어보기 (4)
    • 보안 (7)
    • 설계 (1)
    • 네트워크 (17)
      • HTTP (9)
      • OSI Layers (5)
    • 회고 (31)
      • 연간 회고 (2)
      • 주간 회고 (29)
    • 인프라 (52)
      • 도커 (12)
      • AWS (9)
      • 용어 (21)
      • 웹 성능 (1)
      • 대규모 서비스를 지탱하는 기술 (9)
    • 깃 (7)
    • 빌드 도구 (7)
      • 메이븐 (6)
      • 그레이들 (0)
    • Java (135)
      • 이펙티브 자바 (73)
      • 자바 API (4)
      • 자바 잡지식 (30)
      • 자바 디자인 패턴 (21)
      • 톰캣 (Tomcat) (7)
    • 프레임워크 (64)
      • next.js (14)
      • 스프링 프레임워크 (28)
      • 토비의 스프링 (6)
      • 스프링 부트 (3)
      • JPA (Java Persistence API) (5)
      • Nest.js (8)
    • 프론트엔드 (48)
      • 다크모드 (1)
      • 노드 패키지 관리 매니저 (3)
      • CSS (19)
      • Web API (11)
      • tailwind-css (1)
      • React (5)
      • React 새 공식문서 요약 (1)
      • HTML (Markup Language) (5)
    • 자바스크립트 (108)
      • 모던 자바스크립트 (31)
      • 개념 (31)
      • 정규표현식 (5)
      • 코드 스니펫 (1)
      • 라이브러리 (6)
      • 인터뷰 (24)
      • 웹개발자를 위한 자바스크립트의 모든 것 (6)
      • 팁 (2)
    • Typescript (49)
    • 리눅스와 유닉스 (10)
    • Computer Science (1)
      • Compiler (1)
    • IDE (3)
      • VSCODE (1)
      • IntelliJ (2)
    • 세미나 & 컨퍼런스 (1)
    • 용어 (개발용어) (16)
      • 함수형 프로그래밍 용어들 (1)
    • ORM (2)
      • Prisma (2)
    • NODEJS (2)
    • cypress (1)
    • 리액트 네이티브 (React Native) (31)
    • 러스트 (Rust) (15)
    • 코틀린 (Kotlin) (4)
      • 자바에서 코틀린으로 (4)
    • 정규표현식 (3)
    • 구글 애널리틱스 (GA) (1)
    • SEO (2)
    • UML (2)
    • 맛탐험 (2)
    • 리팩토링 (1)
    • 서평 (2)
    • 소프트웨어 공학 (18)
      • 테스팅 (16)
      • 개발 프로세스 (1)
    • 교육학 (1)
    • 삶의 지혜, 통찰 (1)
    • Chat GPT (2)
    • 쉘스크립트 (1)
    • 컴파일 (2)
    • Dart (12)
    • 코드팩토리의 플러터 프로그래밍 (4)
    • 플러터 (17)
    • 안드로이드 스튜디오 (1)
    • 윈도우즈 (1)
    • 잡다한 백엔드 지식 (1)
    • 디자인 패턴 (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 도커공식문서
  • MSSQL
  • 토비의 스프링
  • 자바스크립트
  • 외래키 제약조건
  • item8
  • 플라이웨이트패턴
  • try-with-resources
  • bean Validation
  • 알고리즘
  • 러스트
  • 싱글턴
  • 객체복사
  • Next.js
  • Javadoc 자바독 자바주석 주석 Comment
  • 메이븐 라이프사이클
  • 느린 쿼리
  • 자바 검증
  • 메이븐 페이즈
  • 참조 해제
  • 싱글톤 패턴
  • item9
  • 추상 팩터리 패턴
  • prerendering
  • 메이븐 골
  • 자료구조
  • 이펙티브자바
  • 자바 디자인패턴
  • 서버리스 컴퓨팅
  • pnpm
  • serverless computing
  • Pre-rendering
  • rust
  • 슬로우 쿼리
  • item7
  • 자바스크립트 인터뷰
  • 팩터리 메서드 패턴
  • 디자인패턴
  • 빈 검증
  • Java
  • 자바
  • 이펙티브 자바 item9
  • 작업기억공간
  • next js app
  • 이펙티브 자바
  • 프로그래머의 뇌
  • 자바스크립트 면접
  • NEXT JS
  • 싱글톤
  • 스프링 검증

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
Jake Seo

제이크서 위키 블로그

자바스크립트/개념

자바스크립트 클로저 (Closure) 란 무엇인가?

2022. 7. 13. 17:39

개요

Closure 란, Lexical Scope 개념에서 스코핑 개념을 조금 더 확장시킨 것이다.

외부 함수가 값을 반환했음에도 Lexical Scope 에 의해 내부 함수에서 외부 함수 스코프에 존재하는 변수에 접근 가능하다는 것이 핵심이다.

클로저 예제 코드 1: 기본 개념

function outerFunction() {
  const outerVariable = "Mozilla";

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

const myFunc = outerFunction();

myFunc();
  • innerFunction() 에서는 outerFunction() 스코프에 존재하는 outerVariable 을 출력(console.log())하려 한다.
  • Lexical Scope 에 의해 내부 함수에서 외부 함수의 변수 값에 접근하는 것은 아무런 문제가 없다.
  • 위 코드에서 눈여겨봐야 할 것은 외부 함수가 반환된 이후에도 외부 함수에 선언되었던 변수에 여전히 접근 가능하다는 것이다.
  • outerFunction() 은 소위 말하는 Function Factory 형태로 구현된 함수이다.
  • 코드를 직접 실행시켜보면 결과를 알 수 있다.

클로저 예제 코드 2: 파라미터를 이용한 클로저

function makeAdder(x) {
  return function (y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2)); // 7
console.log(add10(2)); // 12

파라미터도 Function Scope 에 있는 변수처럼 접근 가능하기 때문에 변수 선언을 하지 않고 이를 이용해서도 클로저 코드를 만들 수 있다.

function makeSizer(size) {
  return function () {
    document.body.style.fontSize = `${size}px`;
  };
}

const size12 = makeSizer(12);
const size14 = makeSizer(14);
const size16 = makeSizer(16);

/*
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
*/

콜백 함수의 함수 팩토리 형태로 만들어보았다.

클로저 예제 코드 3: 자바의 private 멤버 따라하기

const makeCounter = function () {
  let privateCounter = 0;

  function changeBy(val) {
    privateCounter += val;
  }

  return {
    increment() {
      changeBy(1);
    },

    decrement() {
      changeBy(-1);
    },

    value() {
      return privateCounter;
    },
  };
};

const counter1 = makeCounter();
const counter2 = makeCounter();

console.log(counter1.value()); // 0.

counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2.

counter1.decrement();
console.log(counter1.value()); // 1.
console.log(counter2.value()); // 0.
  • 위의 makeCounter() 함수는 3가지의 메서드를 가진 오브젝트를 반환하는데, 3개의 메서드 모두 Lexical Scoping 에 의해 privateCounter 변수 값에 접근이 가능하다.
  • 오직 반환된 오브젝트 내부에 있는 3개의 메서드에 의해서만 접근이 가능하다.
  • 별 것 아닌 것 같은 일이어도 변수를 private 화 할 수 있다는 것은 설계상 매우 엄청난 일이다.
    • 누군가가 privateCounter 변수값을 직접 바꿀 수 있다는 위협 없이 안정적으로 코드를 작성해나갈 수 있다.

여태까지의 예제 코드로 본 클로저의 장점 정리

  • 사실 대부분 객체지향 프로그래밍에 관련된 장점이다.
  • 데이터 은닉과 캡슐화가 용이해진다.

클로저 스코프 체인

클로저는 3개의 스코프가 있다.

  • 로컬 스코프
  • 인클로징(Enclosing) 스코프 (ex. 블록, 함수, 모듈 스코프 등)
  • 글로벌 스코프
// 글로벌 스코프 (global scope)
const e = 10;
function sum(a) {
  return function (b) {
    return function (c) {
      // 인클로징 스코프, 외부 함수 스코프 (outer functions scope)
      return function (d) {
        // 로컬 스코프 (local scope)
        return a + b + c + d + e;
      };
    };
  };
}

console.log(sum(1)(2)(3)(4)); // log 20

익명 함수를 빼면 아래와 같다.

// global scope
const e = 10;
function sum(a) {
  return function sum2(b) {
    return function sum3(c) {
      // outer functions scope
      return function sum4(d) {
        // local scope
        return a + b + c + d + e;
      };
    };
  };
}

const sum2 = sum(1);
const sum3 = sum2(2);
const sum4 = sum3(3);
const result = sum4(4);
console.log(result); //log 20
function outer() {
  const x = 5;
  if (Math.random() > 0.5) {
    const y = 6;
    return () => console.log(x, y);
  }
}

outer()(); // logs 5 6

모듈 예제 코드

// myModule.js
let x = 5;
export const getX = () => x;
export const setX = (val) => {
  x = val;
};
import { getX, setX } from "./myModule.js";

console.log(getX()); // 5
setX(6);
console.log(getX()); // 6

이렇게 모듈을 이용해도, 같은 영역을 공유하기 때문에 모듈 내부에 있는 전역 변수의 레퍼런스를 유지할 수 있다.

// myModule.js
export let x = 1;
export const setX = (val) => {
  x = val;
};
// closureCreator.js
import { x } from "./myModule.js";

export const getX = () => x; // Close over an imported live binding
import { getX } from "./closureCreator.js";
import { setX } from "./myModule.js";

console.log(getX()); // 1
setX(2);
console.log(getX()); // 2

이렇게 Getter 와 Setter 가 다른 모듈 안에 있어도 가능하다.

클로저로 인해 벌어질 수 있는 실수

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email" /></p>
<p>Name: <input type="text" id="name" name="name" /></p>
<p>Age: <input type="text" id="age" name="age" /></p>
function showHelp(help) {
  document.getElementById("help").textContent = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help);
    };
  }
}

setupHelp();

JSFiddle 에서 실행해보기

위 코드만 보고 힌트없이 코드에서 발생하는 문제점을 찾는다면, var 키워드를 이용해 코딩한 경험이 많은 사람일 것이다.

위 코드에서의 핵심적인 문제를 짚자면, onfocus 에 할당된 익명 함수에 들어있는 item.help 의 평가 시점이 콜백함수를 할당할 때가 아니라 실제로 포커스가 들어갔을 때이기 때문이다.

풀어서 설명하자면, onfocus 에 할당한 모든 콜백 메서드는 (원치 않았겠지만) 클로저(closure)이다. var 로 선언된 helpText 는 함수가 끝난 뒤에도 콜백함수 내부에서 참조하고 있기 때문에 스코프가 계속 기억된다.

그래서 콜백함수가 모두 할당된 이후 i 의 값이 증가하면서 다음 루프로 넘어갈 때 showHelp(item.help) 의 item.help 는 스코프가 끝나지 않으므로 계속 같은 메모리 값을 참조하게 된다.

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help);
    };
  }

  console.log("item", item);
}

스코프가 지워지지 않으므로, 위의 코드에서의 console.log(item) 도 정상적으로 마지막으로 사용했던 item 을 출력하게 된다.

결국 item.help 는 모두 같은 메모리 주소를 참조하게 되므로 의도한대로 동작하지 않게 되는 것이다. 해결 방법은 그냥 item 을 선언할 때 const item 으로 선언하는 것 하나만으로 가능하다.

혹은 다른 스코프를 만들어 즉시 평가되도록 하여도 된다.

// ...

for (var i = 0; i < helpText.length; i++) {
  var item = helpText[i];
  document.getElementById(item.id).onfocus = ((txt) =>
    function () {
      showHelp(txt);
    })(item.help);
}

// ...

이렇게 코드를 작성하면, item.help 가 아닌 txt 라는 파라미터의 스코프를 따르기 때문에 파라미터의 스코프는 함수가 끝나면서 만료되어 기존의 item 처럼 계속 남아있지 않는다.

더 간단히 수정하면 아래와 같이 코드를 바꾸어도 스코프가 나뉜다.

function showHelp(help) {
  document.getElementById("help").innerHTML = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];

    setCallback(item.id, item.help);
  }
}

function setCallback(id, txt) {
  document.getElementById(id).onfocus = function () {
    showHelp(txt);
  };
}

결국 이 모든게 lexical scope & closure 라는 개념 때문에 생긴 버그이다.

성능을 위해 고려해봄직한 것

위에서도 잘 나와있다시피 함수는 자신의 스코프와 클로저까지 관리하기 때문에 이러한 클로저를 불필요하게 늘리는 것은 메모리 낭비이자 속도 저하의 원인이 될 수 있다.

새로운 오브젝트나 클래스를 만들 때는 생성자보다는 prototype 을 고려하자.

생성자에 메서드를 두게 되면 오브젝트나 클래스를 생성할 때마다 메서드가 재할당되어버린다.

나쁜 예제

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();

  this.getName = function () {
    return this.name;
  };
  this.getMessage = function () {
    return this.message;
  };
}

클로저 때문에 매 클래스 생성 시에 함수에서 name 과 message 의 스코프를 저장한다. 클로저를 사용하지 않는 방향으로 개선하는 것이 좋다.

개선된 예제

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName() {
    return this.name;
  },
  getMessage() {
    return this.message;
  },
};

더이상 각 오브젝트/클래스의 name 과 message 스코프를 기억하진 않지만, 프로토 타입을 재정의하는 것은 권장되지 않는다.

좋은 예제

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function () {
  return this.name;
};
MyObject.prototype.getMessage = function () {
  return this.message;
};
  • 모든 MyObject 에서 동일하게 사용된다.
  • 오브젝트 생성 시마다 스코프를 기억하지 않는다.
  • 프로토타입을 재정의하지 않는다.

레퍼런스

MDN - Closures

반응형
저작자표시 비영리 (새창열림)

'자바스크립트 > 개념' 카테고리의 다른 글

자바스크립트 호이스팅 매우 간단히 정리  (0) 2022.07.30
자바스크립트 DOMContentLoaded vs load (onload) 의 차이  (0) 2022.07.24
자바스크립트 var , let , const 의 스코프 차이에 대해 알아보자.  (0) 2022.07.13
자바스크립트의 Proxy 란?  (0) 2022.06.30
자바스크립트 fetch API 알아보기 (feat. ajax)  (0) 2022.06.26
    '자바스크립트/개념' 카테고리의 다른 글
    • 자바스크립트 호이스팅 매우 간단히 정리
    • 자바스크립트 DOMContentLoaded vs load (onload) 의 차이
    • 자바스크립트 var , let , const 의 스코프 차이에 대해 알아보자.
    • 자바스크립트의 Proxy 란?
    Jake Seo
    Jake Seo
    ✔ 잘 보셨다면 광고 한번 클릭해주시면 큰 힘이 됩니다. ✔ 댓글로 틀린 부분을 지적해주시면 기분 나빠하지 않고 수정합니다. ✔ 많은 퇴고를 거친 글이 좋은 글이 된다고 생각합니다. ✔ 간결하고 명료하게 사람들을 이해 시키는 것을 목표로 합니다.

    티스토리툴바