Javascript로 개발하다보면, 처음에는 아주 쉽고 재밌게 느껴지며 너무 편하다고생각이 된다.

그러나 일정수준 이상되면 첫번째 한계에 부딪히게 되는데 그게바로 "Closure" 이다.

심지어 clousre에 대한 job interview 대답에 따라 $40k 까지 연봉이 차이가 날수도 있다는 blog posting까지 보았다.

(https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36)

(뇌피셜일수도 있으니 주의하자)

그럼 이토록 중요한 클로저가 무엇인지 다시한번 상기하고 필요성에 대해서 얘기해보고자 한다.

 

 

Closure

 먼저 클로저를 얘기하기전에 var에 대해서 이야기해보자, var로 선언된 변수는 기본적으로 function scope 내에서만 사용되어진다. 

function() {
  var a = 1;
  console.log(a); // works
}    
console.log(a); // fails

 

내가만약 위의 함수를 오류가 없이 동작시키려면 global scope를 사용해야 할것이다.

var a = 1;
function() {
  console.log(a); // works
}    
console.log(a); // works

 

지역변수는 보통 function이 끝나게 되면 메모리에서 사라진다.

그렇다면 우리는 local variable을 계속 참조 할 수 없을까? 이를 closure가 답변해준다.

 

 

클로저는 쉽게말해 persistance한 local variable scope이다. (persistance = 영속적인 한글로 표현하면 좀 어색해진다.)

클로저는 코드의 실행이 function 밖으로 이동했음에도 불구하고 지속되는 persistance local scope이다.
더 쉽게말해서 function scope 밖에서도 그 local variable에서 변수를 reference 할 수 있게 해준다. 
(객체지향적 설계를 가능하게 한다.)

다음을 코드를 보면 더 이해가 잘 될것이다.

outer = function() {
  var a = 1;
  var inner = function() {
    console.log(a);
  }
  var add = function() {
  	a += 1;
  }
  return {inner,add}; // this returns a function
}

var fnc = outer(); // execute outer to get inner 
fnc.inner();
fnc.add();
fnc.inner();

//output
//1
//2

위 코드에서처럼 a는 local variable임에도 불구하고 영속성을 갖는다. 이게 클로져이다.

JavaScript에서는 함수가 처음 선언 될 때 var a가 hoisting 되고, 함수가 계속 존재하는 한 var a도 지속되어진다.

a는 outer scope에 속한다. inner scope는 outer scope에 대한 참조가 있다. fnc는 inner scope를 가리키는 변수다. fnc가 지속되는 한 a또한 사라지지 않고 존재한다. closure 안에 있다.

이를 좀더 유식하게 말하면 "특정 함수가 참조하는 변수들이 선언된 렉시컬 스코프(lexical scope)는 계속 유지되는데, 그 함수와 스코프를 묶어서 closure 라고한다.

 

 

실질적인 closure에 관한 예제

먼저, 우리가 버튼을 클릭할때마다, 버튼이 클릭된 횟수를 세는 기능을 만든다고 가정해보자.

<button onclick="updateClickCount()">click me</button>  

 

1) Global variable을 쓰면 가장 쉽게 해결가능하다.

var counter = 0;

function updateClickCount() {
    ++counter;
    // do something with counter
}

 

그러나 counter라는 글로벌변수는 쉽게 노출되며, updateClickCount라는 함수의 호출없이 ++ 시킬수 있다.

 

2)그러면 counter function scope 내부로 옮긴다면?

function updateClickCount() {
    var counter = 0;
    ++counter;
    // do something with counter
}

counter는 항상 1로 될것이다. counter는 persistance한 local variable 이 아니기 때문이다.

 

3)Nested functions를 쓰면?

function countWrapper() {
    var counter = 0;
    function updateClickCount() {
    ++counter;
    // do something with counter
    }
    updateClickCount();    
    return counter; 
}

 당신이 외부 scope에서 updateClickCount 함수에 접근할수 있고, counter=0라는 선언을 한번만 호출할 수 있다면, 가장 좋은 방법일 것이다.

 

정답) Closure가 출격한다면 어떻게 될까? C L O S U R E ! (with IIFE)

 var updateClickCount=(function(){
    var counter=0;

    return function(){
     ++counter;
     // do something with counter
    }
})();

 

위처럼 한다면 우리가 처음 의도한대로 동작할 것이다.
local variable을 영속성을 갖게할수 있고 또 local variable을 객체지향적으로 encapsulation 할 수 있게된다.

이에 더하여 counter변수는 global scope에서 hoisting 되지 않으므로 변수의 충돌 또한 방지해준다.

 

<script>
var updateClickCount=(function(){
    var counter=0;

    return function(){
      ++counter;
      document.getElementById("spnCount").innerHTML=counter;
    }
  })();
</script>

<html>
  <button onclick="updateClickCount()">click me</button>
    <div> you've clicked 
  		<span id="spnCount"> 0 </span> times!
  	</div>
</html>

 

 

https://stackoverflow.com/questions/2728278/what-is-a-practical-use-for-a-closure-in-javascript

 

What is a practical use for a closure in JavaScript?

I'm trying my hardest to wrap my head around JavaScript closures. I get that by returning an inner function, it will have access to any variable defined in its immediate parent. Where would this be

stackoverflow.com

 

 

PS. 우리는 let으로 문제를 해결했던 for 문 딜레마도 closure를 통해 해결할 수 있다.

var i = 0;
for(i = 0; i<3; i++) {
	(function(index){
    	cosnole.log(My value : index);
    }(i))
}

 

 

Scenario


최근에 es6로 프로그래밍을 하면서, let을 많이쓰게되었고, let은 단순히 지역변수라고만 생각했엇다.

근데 다시 곰곰히 생각해보니 var도 function scope 이므로 지역변수로 사용할 수 있지않은가?

라는 생각이 들었다. 책에서 읽었던 내용중에 let은 bracket scope 였던게 생각이났다.

function run() {
  var foo = "Foo";
  let bar = "Bar";

  console.log(foo, bar);

  {
    let baz = "Bazz";
    console.log(baz);
  } //사실 braket scope을 그닥 많이쓰진 않는다.

  console.log(baz); // ReferenceError
} 

run();

 사실 나의경우는 component 기반 es6 기반 프레임워크&라이브러리로 javscript를 쓰다보면 bracket scope을 그닥 많이 쓰진않는다. 그럼 let이라는게 왜 등장을 한지 배경을 좀 조사해보았다.

 

var funcs = [];

for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  funcs[j]();
}
//output: expect 0, 1, 2 but
//My value: 3
//My value: 3
//My value: 3

위 예제의 경우 let을 써야하는 이유가 더 와닿는다. (실제로도 나는 let은 for문에서 가장많이쓰는것같다.)

 

Solution


먼저 var로 선언된 변수를 이해 하려면, hoisting을 이해해야 한다. 

 

hoisting 


사전적의미로 들어올리다 이다. 자바스크립트의 hoisting을 이해하려면 다음 소스코드부터 보자.

 

function run() {
  console.log(foo); // undefined -- ReferenceError가 나지않는다.
  var foo = "Foo";
  console.log(foo); // Foo
}

run();

 

 위 소스코드에서 3번째 줄에서 오류가 나지않는 이유는 hoisting 되었기 때문이다. 변수가 생성된 시점은 3번째줄이지만,  scope 상단으로 hosting 되어진다. 이는 변수들이 선언되기 전에 스코프내에서 접근할 수 있다는 것을 의미한다.
(var는 초기화 되기 전까지는 블록의 시작에 "temporal dead zone"에 있다고들 말한다.)  

 

function checkHoisting() {
  console.log(foo); // ReferenceError
  let foo = "Foo";
  console.log(foo); // Foo
}

checkHoisting();

그러나 위의 소스코드는 error가 난다. 이유는 let은 호이스팅 되지 않기 때문에 선언되기 전에 foo에 접근을 시도하기 때문이다. 이는 아주 중요한 차이점을 갖는다.

 

주의점

gloval scope에선 let을 쓰지 않도록 주의해야한다.

var foo = "Foo";  // globally scoped
let bar = "Bar"; // globally scoped

console.log(window.foo); // Foo
console.log(window.bar); // undefined

 

또한 var는 재정의할 수있으므로 주의해서 써야한다.

var foo = "foo1";
var foo = "foo2"; // No problem, 'foo' is replaced.

let bar = "bar1";
let bar = "bar2"; // SyntaxError: Identifier 'bar' has already been declared

 

 

그리고 아까위의 for문은 다음과 같이 고칠 수 있다.

var funcs = [];
// let's create 3 functions
for (let i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}
//output
//My value: 0
//My value: 1
//My value: 2

 

*주의 : ie에서는 let을 지원하지않으므로, webpack과 같은 bundle을 쓰지않는다면 쓰지말자.

 

Scenario


최근 git cli를 사용하려고 노력중이다보니, Source tree를 사용하지 않고 직접 cmd를 치다보면 merge를 해야할지 rebase를 해야할지, 헷갈리때가 있다. 또한 옵션에 대해서도 잘 모르고 써었는데 한번 정리해보자 한다.

 

 

Solution


기본적으로 git pull = git fetch + git merge 이다.

그러면 어느때 git merge를 쓰고 어느때 git rebase를 써야할까?

 

Merge

현재 C2 parent에서 brach가 둘로 분기되었다 가정하자
master로 checkout 한후 merge 하였다. (checkout master && merge experiment)

위의 경우가 통상적인 merge 방법이나 이렇게 되면 작업 history가 가시적일 순 있어도 branch가 많아지면 많아질수록 알아보기가 어려워진다. 사람들은 이러한 이유로 rebase를 쓴다.

 

rebase

똑같은 상황 가정

 

 

'experiment에서 master rebase (checkout experiment && rebase master) 

master 브랜치를 Fast-forward시킨다 ('앞으로 진행한' 커밋인 master 브랜치 포인터는 최신 커밋으로 이동한다. 이런 Merge 방식을 'Fast forward'라고 부른다.)

 

완료후 정리된 커밋모습

 

위와같이 c3` 커밋메세지 기반으로 history가 정리됨을 알수있다.

tip) git rebase [basebranch] [topicbranch] 처럼 인자를 준다면 일일히 checkout 하지 않고 rebase할 수있다.

위 그림같은 경우는
$ git rebase master experiment

 

 

 Merge

Rebase 

 특징

 -  branch의 최종 결과만을 가지고 합병

 - 지정한 브랜치를 베이스로 기준 삼아 합병

 - 중복수정된 로그가 남지않는다.

 - 브랜치의 변경사항을 순서대로 다른 브랜치에 적용하면서 합병

 장점

 - 이해하고 사용하기 쉬움

 - 브랜치 컨텍스트 유지

 - history가 단순해짐

 - branch가 많을때 커밋을합치는 직관적인방법

 단점

 - 히스토리가 난잡

 - 커밋 순서대로 rebase를 하는데, 각 커밋마다 충돌해소를 순서대로 해주어야하여 복잡함

 

추가유용 명령어
- FILE 되돌리기 및 삭제편

git checkout [-- 파일명]
아직 스테이징이나 커밋을 하지 않은 파일의 변경내용을 취소하고 이전 커밋상태로 돌린다. svn에서 revert와 동일하다 (그냥 git checkout branch를 할경우 현재 활성화된 브랜치를 바꾸는 명령어이다)  

git checkout -- . wd에 수정된내용 모두 되돌림

git diff [--cached]
스테이징영역과 현재 작업트리의 차이점을 뵤어준다. --cached 옵션을 추가하면 스테이징영역과 저장소의 차이점을 볼 수 있다. git diff HEAD를 입력하면 저장소, 스테이징영역, 작업트리의 차이점을 모두 볼 수 있다. 파라미터로 log와 동일하게 범위를 지정할 수 있으며 --stat를 추가하면 변경사항에 대한 통계를 볼 수 있습니다.
git diff --ours 머지이전과 머지이후 결과비교

git reset — hard HEAD^
commit한 이전 코드 취소하기

git merge --abort 머지 취소하기(커밋이나 stash 하지않는 존재시 못함)

git config — global user.name “user_name ”
git 계정Name 변경하기

git config — global user.email “user_email” 
git 계정Mail변경하기

git stash / git stash save “description”
작업코드 임시저장하고 브랜치 바꾸기

git stash pop
마지막으로 임시저장한 작업코드 가져오기

git reset HEAD [-- filesName] stage에 있는 파일 내리기

git reset — soft HEAD^ 
코드는 살리고 commit만 취소하기

git reset — merge 
merge 취소하기

git reset — hard HEAD && git pull  git 코드 강제로 모두 받아오기

git reset --hard HEAD 머지하기이전상태로 모두되돌림

git reset --[hard] [mixed] [soft] 

--hard: reset하기 전까지 했던 staging area, working directory의 작업까지 모두 reset!
(모든 게 잘못됐어! 나 돌아갈래~ 꽃피던 때부터 정갈하게 다시 해보자!)
--mixed(default): staging area은 reset, reset하기 전까지 했던 working directory의 작업은 남겨둠.
(현재 작업물은 지우긴 싫고, 이전 버전으로 돌아가서 add할지 말지 결정해야 할 때)
--soft: reset하기 전까지 했던 staging area, working directory의 작업은 남겨둠.
(reset한 버전과 현재까지의 작업을 합쳐 새로운 버전 만들 때)
git reset --hard 커밋된 파일빼고 모두삭제
git clean -df 커밋,stage 되지 않은 파일 폴더 모두삭제 [untrack 중인] (매우 유용) 

 

-- 유용한명령어들

git add .
git commint -m "{msg}"
git remote -v  --list all
git remote set-url origin "{repo-url}"
git remote add origin "{repo-url}"
git remote remove origin
git push --set-upstream origin master 초기
git config --list
git commit --author "asd<asd@gmail.com>" -m
git log --graph --oneline --all  로그를 커밋메시지는 한줄로 그래프형태로 모두보기
git log --graph --oneline --all --pretty="format:[%h]%s - %an" 커미터지정해서 보기

+git stash

stash는 다시 한번 말하지만 하고 있던 작업을 잠시 담아두는 역할을 한다.

따라서 명령어는 크게 두 가지만 기억하면 된다.

git stash(=git stash save) : 하던 작업을 저장하고 가장 최근 commit상태로 만든다.

git stash pop 또는 git stash apply : 저장되어 있는 작업중 가장 최근 stash를 가져온다.

이 외에 명령어 옵션들

git stash list : stash 목록을 봄 stash@[숫자]형식으로 보여지며 0번이 가장 최근 1,2,3... 이런식으로 밀림

git stash drop[stash@[숫자]] : stash를 따로 지정하지 않으면 최신의 stash삭제

* git stash pop은 git stash apply + git stash drop을 같이 한 것과 같은 효과임.

  즉, git stash pop은 한번 불러오면 stash 목록에 저장한 시점이 삭제되어있고 git stash apply는 해당 stash를 불러와도 여전히 list에 남아 있음.

 

+ Recent posts