Conventional Commits?

커밋 메세지에 사용자와 기계 모두가 이해할 수 있는 의미를 부여하기 위한 스펙

명확한 커밋 히스토리를 생성하기 위한 간단한 규칙을 제공

커밋 히스토리를 이용하여 더 쉽게 자동화된 도구를 만듦

이 컨벤션은 커밋 메세지에 신규 기능 추가, 문제 수정, 커다란 변화가 있음을 기술함으로써 유의적 버전(Sementic Versioning)과 일맥상통

git 으로 commit 시에 일괄된 양식을 유지 → 그 양식을 바탕으로 버전 관리나 Change Log 를 자동으로 만들 수 있음

커밋 메시지 구조

<타입>[적용 범위(선택 사항)]: <설명>

[본문(선택 사항)]

[꼬리말(선택 사항)]

커밋 메시지 구조적 요소

Git hook에 Conventional Commit 적용하기

Commit 메시지를 아름답고 정갈하게 유지하기 위해 conventional commit을 git hook에 적용하는 과정을 소개하려고 한다.

1. 패키지에 husky 적용하기

husky 가 뭔가요?

husky는 git hook을 손쉽게 제어하도록 도와주는 npm 라이브러리이다.

git hook?

git을 쓰다가 특정 이벤트(커밋할 때, 푸시할 때 등등)가 벌어졌을 때, 그 순간에 ‘갈고리’를 걸어서 특정 스크립트가 실행되도록 도와주는 것!

물론 husky를 쓰지 않더라도 git hook을 설정할 수 있는 공식적인 방법은 따로 있다.
.git/hooks 폴더에 들어가서 스크립트를 작성하면 된다.

그러나 .git/hooks 폴더 안에 스크립트 파일을 넣게 되면 그 파일은 git에 기록되지 않아서 따로 관리해야 한다는 단점이 있다.

git hook으로 npm scripts를 제어하고 싶을 때, 예컨대 npm test 등의 명령어를 써야 한다면 스크립트를 작성하는 게 번거롭다.

husky는 굳이 .git/hooks 폴더를 건드리지 않고도 git hook 스크립트를 제어할 수 있게 도와준다.

husky 설치하기

npx husky-init && npm install

package.json 파일에는 아래와 같은 script가 추가된 것을 확인할 수 있다.

"prepare": "husky install"

그리고, .husky 디렉토리가 생성된 것도 확인할 수 있다.

.husky/pre-commit 파일이 생성되었는데, 기존에 test 명령어가 있었기 때문에 이를 감지하여 아래와 같이 추가되었다.

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npm test

2. 패키지에 commitlint 적용하기

commitlint 는 뭔가요?

commit 에 대한 lint를 확인하여 성공/실패를 리턴해주는 도구이다.

commitlint 설치하기

npm install -D @commitlint/cli @commitlint/config-conventional

commitlint config 추가하기

루트 디렉토리에 commitlint.config.js 파일을 추가해준다.

module.exports = { extends: ['@commitlint/config-conventional'] };

commitlint의 기본 컨벤션

module.exports = {
    parserPreset: 'conventional-changelog-conventionalcommits',
    rules: {
        'body-leading-blank': [1, 'always'],
        'body-max-line-length': [2, 'always', 100],
        'footer-leading-blank': [1, 'always'],
        'footer-max-line-length': [2, 'always', 100],
        'header-max-length': [2, 'always', 100],
        'subject-case': [
            2,
            'never',
            ['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
        ],
        'subject-empty': [2, 'never'],
        'subject-full-stop': [2, 'never', '.'],
        'type-case': [2, 'always', 'lower-case'],
        'type-empty': [2, 'never'],
        'type-enum': [
            2,
            'always',
            [
                'build',
                'chore',
                'ci',
                'docs',
                'feat',
                'fix',
                'perf',
                'refactor',
                'revert',
                'style',
                'test',
            ],
        ],
    },
};

3. commit-msg hook 적용하기

commit-msg 훅은 최종적으로 커밋이 완료되기 전에, 프로젝트 상태나 커밋 메시지를 검증하기 위해 사용한다.

husky hook 에 commit-msg 추가하기

npx husky add .husky/commit-msg 'npx --no-install commitlint --edit $1'

이렇게 하면, commitlint 의 컨벤션으로 husky가 commit-msg 훅을 실행한다.

commit-msg hook 동작 확인하기

그럼 어디한번 동작하는지 확인해볼까.

우선 잘못된 커밋 메시지를 넣어보자.

git commit -m 'hello-world'

두둥탁- 짠-.

subject 가 비어있으면 안되고, type 도 비어있으면 안된다고 지적해준다.

이번엔 제대로된 메시지를 넣어보자.

git commit -m 'test: hello world'

아주아주 잘되는 것을 확인할 수 있다.

git kraken 에서 동작 확인하기

야심차게 git kraken 에서도 잘못된 커밋메시지로 커밋을 시도해봤다.

그런데 웬걸 😱

커밋이 너무 잘된다... 젠장.

commit-msg 훅 로그를 들여다보니.

별 말도 없다. 어쩌란 건지..

그래서 issue tracking 을 해보니, 아래와 같은 문제점이 있다고 한다.

Husky v5 and Gitkraken #875

  • 버전 7.5부터 Gitkraken은 core.hookspath를 지원하지 않으며 .git/hooks을 사용할 것입니다.이 문제를 해결하는 방법은 매우 간단합니다.
    .git/hooks 에서 허스키의 디렉토리로의 심볼릭 링크를 만듭니다.
    최선의 방법은 아니지만 내가 찾은 유일한 방법입니다.

      $ rm -rf .git/hooks && ln -s ../.husky .git/hooks

    You may add this command to your package.json postinstall script, alongside husky install:

  • { "scripts": { "postinstall": "husky install && rm -rf .git/hooks && ln -s ../.husky .git/hooks" } }

  • From you project root:

  • 참고로, git 공식 문서에 이런 글이 있다.
    By default the hooks directory is $GIT_DIR/hooks, but that can be changed via the core.hooksPath configuration variable.

  • husky v5부터는 기본 git의 경로 ( .git/hooks)가 아닌 사용자 지정 후크 경로 ( .husky) 가 사용됩니다 . 이것은 로컬 git 구성 core.hookspath키 설정을 통해 수행됩니다.

하...

이렇게까지 해서 맥 Big Sur OS 에도 최적화되지 못한 채, 커밋 날릴때마다 7초 이상 기다리게 하더니, 이제는 깃훅 path 커스텀도 막아버리는 무례한 깃크라켄의 만행을, 돈을 내가면서 참아줘야 하는걸까.

결국 했다.

gitkraken 을 사용하지 않는 컨트리뷰터들을 위해 아래 postinstall script 는 추가하지 않고, 로컬에서만 .git/hooks 에 심볼링 링크를 걸어주는 것으로 끝냈다.

4. 컨벤션에 맞게 커밋하기

컨벤션에 맞게 커밋하는 것을 도와주는 commitizen cli 도구를 사용하려고 한다.

commitizen 설치하기

npm i -D commitizen

commitizen 어댑터 구성하기

commitizen으로 커밋을 만들기 위한 방식은 어떤 어댑터를 사용하느냐에 따라 달라진다.

여기에서는 cz-conventional-changelog 어댑터 사용할 것이다.

npx commitizen init cz-conventional-changelog --save-dev --save-exact

위 명령어는 아래 3가지를 수행한다.

  1. cz-conventional-changelog 어댑터를 설치한다.
  2. 그리고 dev dependency에 추가한다.
  3. 아래처럼 config.commitizenpackage.json 에 추가한다.
"config": {
    "commitizen": {
      "path": "cz-conventional-changelog"
    }
  }

이렇게 설정하면, 커밋하려고 할 때 사용할 어댑터를 commitizen에게 알려준다.

commitizen script 추가하기

package.json에 아래와 같은 script 를 추가한다.

husky와 함께 쓰는 경우는 commit 명령어로 쓰지 말라는 경고가 있으니 주의하자.

"scripts": {
...
    "cm": "cz"
  }
npx cz

or

npm run cm

위 명령어를 통해 commitizen으로 커밋을 정형화된 포맷으로 할 수 있다.

그러나, git commit 명령어를 실행시켰을 때, commitizen 을 먼저 실행시킬 수 있도록 설정한다면, 굳이 이 명령어를 쓰지 않아도 된다.

커밋 시 commitzen 실행하도록 husky에 훅 추가하기

npx husky add .husky/prepare-commit-msg 'exec < /dev/tty && node_modules/.bin/cz --hook || true'

commitizen 으로 커밋하기

git commit
  1. 원하는 type 선택하기
  2. 필요한 설명 입력하기
  3. 이슈에 연결하기(선택사항)
  4. 결과 확인

5. commitizen 어댑터 변경하기 (선택사항)

엇, 그런데 commitizen 에서 제공하는 어댑터 중에 commitlint 를 사용하는 것이 있다는 것을 뒤늦게 알아버렸다.

위에 commit-msg 훅에서 commitlint를 사용하고 있어서, 일관성 유지에는 commitlint를 사용하는 것이 좋을 것 같아서 commitlint 어댑터로 바꿔보겠다.

기존 어댑터 삭제하기

위에서 추가해준 cz-conventional-changelog 어댑터를 삭제해주자.

npm uninstall -D cz-conventional-changelog

새로운 어댑터 추가하기

npm i -D @commitlint/prompt

commitizen 어댑터 설정 변경하기

아래처럼 package.json 에서 config.commitizen 을 변경해주자.

"script": {
...
"cm": "git-cz"
},
"config": {
    "commitizen": {
       "path": "./node_modules/@commitlint/prompt"
    }
  }

변경된 어댑터 동작 확인하기

git commit

전혀 다른 방식으로 커밋이 진행되는 것을 확인할 수 있다.

마치며.

마지막에 바꾼 어댑터.
써보니까 너무 별로여서 다시 기존 어댑터로 롤백했다.
ㅎㅎ..

그로부터 며칠 뒤.
prepare-commit-msg 훅을 뻈다.
이제 대충 커밋 컨벤션이 머리에 있는데 저 과정으로 커밋하는게 영 불편했다.
그래서 필요한 경우에는 npm run cm 으로 실행시키는 것으로.

References

https://www.huskyhoochu.com/npm-husky-the-git-hook-manager/

https://git-scm.com/book/ko/v2/Git맞춤-Git-Hooks

https://blog.cookapps.io/guide/conventional-commits/

https://www.conventionalcommits.org/ko/v1.0.0/

https://github.com/angular/angular/blob/22b96b9/CONTRIBUTING.md#-commit-message-guidelines

'Programming > Git' 카테고리의 다른 글

husky로 git hook에 conventional commit 적용하기  (0) 2021.05.15

실행 컨텍스트 정의

실행 가능한 코드를 형상화하고 구분하는 추상적인 개념

= 실행 가능한 코드가 실행되기 위해 필요한 환경

자바스크립트 엔진은 실행 가능한 코드를 실행하기 위해 필요한 정보를 형상화하고 구분하기 위해 실행 컨텍스트를 물리적 객체의 형태로 관리한다.

실행 가능한 코드

  • 전역 코드 : 전역 영역에 존재하는 코드
  • 함수 코드 : 함수 내에 존재하는 코드
  • Eval 코드 : eval 함수로 실행되는 코드
Eval 함수?
문자로 표현 된 JavaScript 코드를 실행하는 함수.
인자로 받은 코드를 caller의 권한으로 수행하므로, 제 3자 코드가 eval()이 호출된 위치의 스코프를 볼 수 있으며, 비슷한 함수인 Function으로는 실현할 수 없는 공격이 가능하여 절대 쓰지 않는 것이 좋다.

실행에 필요한 정보

  • 변수 : 전역변수, 지역변수, 매개변수, 객체의 프로퍼티
  • 함수 선언
  • 변수의 유효범위(Scope)
  • this

정리하면

  1. 변수, 함수 선언, 스코프, this는 ****전역, 함수 코드를 실행하기 위해 필요한 정보이다.
  2. 실행 컨텍스트는 전역 코드, 함수 코드를 실행하기 위한 환경이다.

실행 컨텍스트 스택

앞으로 실행 가능한 코드를 편의상 '함수'라고 하겠다.

함수를 실행하면, 실행 컨텍스트가 생성된다.

그 안에서 또 다른 함수가 실행되면 그 위에 실행 컨텍스트가 생성되어, 스택 구조로 콜스택 메모리에 쌓인다.

그리고 함수 실행이 종료되면, 실행 컨텍스트가 파기되는 LIFO 구조다.

 

 

var x = 'xxx';

function foo () {
  var y = 'yyy';

  function bar () {
    var z = 'zzz';
    console.log(x + y + z);
  }
  bar();
}
foo();

 

위 코드의 실행 컨텍스트 스택은 아래 그림과 같다.

  1. 전역 컨텍스트(Global EC)로 컨트롤 이동 (전역 컨텍스트는 그냥 가장 먼저 실행되는 컨텍스트라고 이해하면 된다)
  2. foo() 함수 실행으로 foo() 실행 컨텍스트 스택이 생성 → foo() 실행 컨텍스트로 컨트롤 이동
  3. foo() 함수 내에서 bar() 함수 실행으로 bar() 실행 컨택스트 스택이 생성 → bar() 실행 컨텍스트로 컨트롤 이동
  4. bar() 함수 실행 종료 → bar() 실행 컨텍스트 파기 → 이전 컨텍스트인 foo()로 컨트롤 이동
  5. foo() 함수 실행 종료 → foo() 실행 컨텍스트 파기 → 이전 컨텍스트인 전역 컨텍스트로 컨트롤 이동

실행 컨텍스트의 3가지 프로퍼티

실행 컨텍스트는 물리적으로는 객체의 형태를 가지며 아래의 3가지 프로퍼티를 소유한다.

  • 활성 객체(변수 객체, Variable Object, VO)
  • 스코프 체인(Scope Chain)
  • this

실행 컨텍스트 생성 및 실행 과정

  1. 활성 객체(변수 객체) 생성
  2. arguments 객체 생성
  3. 스코프 정보 생성
  4. 변수 생성
  5. this 바인딩
  6. 코드 실행
function execute(param1, param2) {
	var a = 1, b = 2;

	function func() {
		return a + b;
	};

	return param1 + param2 + func();
};

execute(3, 4);

활성 객체(변수 객체, Variable Object, VO) 생성

활성 객체, 즉 변수 객체는 실행에 필요한 어려가지 정보를 담을 객체이다. 이 객체에는 변수, 매개변수(parameter), 전달인자(arguments), 함수 선언(함수 표현식은 제외)이 저장된다.

 

arguments 객체 생성

자바스크립트에서는 함수를 호출할 때 암묵적으로 arguments 객체가 함수 내부로 전달된다. 넘긴 인자들이 배열 형태로 저장된 객체이다.

특이한 점은, arguments 객체는 배열이 아니라 유사 배열 객체이다. 유사배열 객체는 객체임에도 불구하고, apply() 를 통해서 자바스크립트의 표준 배열 메소드를 사용하는게 가능하다. 그러나 배열 메소드를 바로 사용하는 것은 불가능하다.

arguments 객체는 다음과 같이 세 부분으로 구성되어 있다.

  • 함수 호출 시 넘겨진 인자 (배열 형태)
  • length 프로퍼티
  • callee 프로퍼티: 현재 실행중인 함수의 참조값

스코프 정보 생성

스코프 정보는 현재 컨텍스트의 유효 범위를 나타낸다. 스코프 정보는 현재 실행 중인 실행 컨텍스트 안에서 linked list와 유사한 형식으로 만들어진다.

이 리스트를 '스코프 체인'이라고 한다.

스코프 체인

[[scope]] 프로퍼티로 참조되는 스코프 정보들의 linked list이다.

스코프 체인은 현재 컨텍스트의 변수 뿐만 아니라, 상위 실행 컨텍스트의 변수도 접근이 가능하다. 이 리스트에서 찾지 못한 변수는 결국 정의되지 않은 변수에 접근하는 것으로 판단하여, 에러를 검출한다.

현재 생성된 활성 객체(변수 객체)가 스코프 체인의 가장 앞에 추가된다.

변수 생성

현재 실행 컨텍스트 내부에서 사용되는 지역 변수들이 생성된다.

  1. 호출된 함수 인자, param1, param2 프로퍼티가 만들어지고 그 값이 할당된다. 만약 값이 넘겨지지 않았다면, undefined 가 할당된다.
  2. 함수 내부에 정의된 변수 a, b와, 함수 func가 생성된다. 이 과정에서는 변수나 내부 함수를 단지 메모리에 생성할 뿐이다. 즉, 변수 a, b에는 각각 undefined 가 할당된다. 그렇다면 초기화는 언제 이루어질까. 각 변수나 함수에 해당하는 표현식이 실행되면 초기화가 이뤄진다.

왜 함수는 초기화까지 이뤄진거죠?
이는 func 함수가 함수 선언식으로 작성되었기 때문에 그렇다. 
함수 표현식은 변수 객체가 모두 만들어진 이후에 코드가 실행되는 시점에 읽힌다. 그러나 함수 선언식은 초기화 단계에서 읽힌다.

 

this 바인딩

this 키워드를 사용하는 값이 할당된다. 여기서 this가 참조하는 객체가 없으면 전역 객체를 참조한다.

 

코드 실행

하나의 실행 컨텍스트가 생성되고, 변수 객체가 만들어진 후에 드디어 코드에 있는 여러 표현식의 실행이 이루어진다.

이 과정에서 변수의 초기화 및 연산, 또 다른 함수 실행 등이 이뤄지는 것이다. 예제 코드의 변수 a, b 에 각각 1, 2 값이 할당된다.

참고로, 전역 실행 컨텍스트는 일반적인 실행 컨텍스트와는 약간 다르다. arguments 객체가 없으며, 전역 객체 하나만을 포함하는 스코프 체인이 있다. 또한 전역 실행 컨텍스트에서는 변수 객체가 곧 전역 객체이다. 따라서, 전역적으로 선언된 함수와 변수가 전역 객체의 프로퍼티가 된다.

Reference

프로토타입(Prototype) 뽀개버리기!! 

 

함수도 객체다

함수의 기본 기능인 코드 실행뿐만 아니라, 함수 자체가 일반 객체처럼 프로퍼티들을 가질 수 있다.

function add(x, y) { 
return x+y; 
}

add.status = 'OK';
console.log(add.status)
  1. add( ) 함수를 생성할 때 함수 코드는 함수 객체의 [[Code]] 내부 프로퍼티에 자동으로 저장된다(이것은 ECMAScript 명세서를 참조한 것이다* ).
  2. add() 함수에 마치 일반 객체처럼 status 프로퍼티를 생성하고 저장한 것을 확인할 수 있다.
  3. status 프로퍼티도 일반 객체에서의 접근 방식처럼 add.status를 이용해 접근 가능하다. 이처럼 자바스크립트에서 함수는 특정 기능의 코드를 수행하는 역할뿐만 아니라, 일반 객체처럼 자신의 프로퍼티를 가질 수 있는 특별한 객체라고 볼 수 있다.

함수는 객체이기 때문에, 일반 객체처럼 취급될 수 있다.

따라서 다음과 같은 동작이 가능하다.

  • 리터럴에 의해 생성
  • 변수나 배열의 요소,객체의 프로퍼티 등에 할당 가능
  • 함수의 인자로 전달 가능
  • 함수의 리턴값으로 리턴 가능
  • 동적으로 프로퍼티를 생성 및 할당 가능

이와 같은 특징 때문에 함수를 일급 객체라고 부른다.

더보기

일급 객체(First Class)

일급 객체라는 말은 컴퓨터 프로그래밍 언어 분야에서 쓰이는 용어로서, 위에 나열한 기능이 모두 가능한 객체를 일급 객체라고 부른다. 자바스크립트 함수가 가지는 이러한 일급 객체의 특성으로 함수형 프로그래밍이 가능하다.

정리하면,

자바스크립트 함수의 기능은 C나 자바와 같은 다른 언어 함수의 기능과 거의 비슷하다.

 

하지만 기본적인 기능 외에도, 자바스크립트에서 함수를 제대로 이해하려면, 함수가 일급 객체라는 것을 아는 것이다.

즉, 함수가 일반 객체처럼 값으로 취급된다는 것을 이해해야 한다. 이 때문에 함수를 변수나 객체, 배열 등에 값으로도 저장할 수 있으며, 다른 함수의 인자로 전달한다거나 함수의 리턴값으로도 사용 가능하다는 것을 알아야 한다.

하지만 일반 객체와는 조금 다르다

무엇이 다를까?

함수 객체만의 표준 프로퍼티가 정의되어 있다.

function add(x, y) { 
    return x+y; 
}

console.dir(add);

 

arguments, caller, length 등과 같은 다양한 프로퍼티가 기본적으로 생성된다. 이러한 프로퍼티들이 함수를 생성할 때 포함되는 표준 프로퍼티다.

참고로 ECMA5 스크립트 명세서에는 모든 함수가 length와 prototype 프로퍼티를 가져야 한다고 기술하고 있다.

  • arguments: 호출 시 전달되는 인자 값
  • caller: 자신을 호출한 함수를 나타낸다
  • name: 함수의 이름
  • length: 함수가 정상적으로 실행될 때 기대되는 인자의 개수
  • __proto__: 부모 역할을 하는 프로토타입 객체를 가리킨다. = [[Prototype]] = __proto__ → Function.prototype
  • prototype: 함수가 생성될 때 만들어지며, 단지 constructor 프로퍼티 하나만 있는 객체를 가리킨다. 그리고 prototype 프로퍼티가 가리키는 프로토타입 객체의 유일한 constructor 프로퍼티는 자신과 연결된 함수를 가리킨다. 즉, 자바스크립트에서는 함수를 생성할 때, 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 각각 prototype 과 constructor 라는 프로퍼티로 서로를 참조하게 된다.

함수 객체는 항상 부모 역할을 하는 객체를 가리킨다: 암묵적 프로토타입 링크

모든 자바스크립트 객체는 자신의 프로토타입을 가리키는 [[Prototype]] 이라는 내부 프로퍼티를 가진다.크롬 브라우저는 이 프로퍼티가 __proto__ 프로퍼티로 구현되어 있다.

이것이 가리키는 부모 역할을 하는 프로토타입 객체는 Function.prototype 객체이다.

 

그리고 모든 함수는 Function Prototype 객체의 프로퍼티나 메소드를 마치 자신의 것처럼 상속받아 그대로 사용할 수 있다. - constructor, toString(), apply(), call(), bind()

 

그런데 ECMAScript 명세서에는 Function.prototype은 함수라고 정의하고 있다.
그리고 함수 역시 객체이므로, 자신의 부모 역할을 하는 프로토타입 객체를 가리킨다.
그렇다면 이러한 규칙에 의해 Function.prototype이 함수니까, 이것도 Function.prototype 객체, 즉, 자기 자신을 부모가 갖는 것인가?

 

ECMAScript 명세서에는 예외적으로 Function.prototype 함수 객체의 부모는 자바스크립트의 모든 객체의 조상격인 Object.prototype 객체라고 설명하고 있다.
때문에 Function Prototype 객체의 proto 프로퍼티는 Object.prototype 객체를 가리킨다.

 

함수는 Prototype 객체랑 같이 다닌다: prototype 프로퍼티

함수 객체의 prototype 프로퍼티는 함수가 생성될 때 만들어진다.

이 프로퍼티는 단지 constructor 프로퍼티 하나만 있는 객체를 가리킨다.

 

그리고 prototype 프로퍼티가 가리키는 프로토타입 객체의 유일한 constructor 프로퍼티는 자신과 연결된 함수를 가리킨다.

즉, 자바스크립트에서는 함수를 생성할 때, 함수 자신과 연결된 프로토타입 객체를 동시에 생성하며, 이 둘은 각각 prototype 과 constructor 라는 프로퍼티로 서로를 참조하게 된다.

 

예제로 살펴보자.

function myFunction() { 
    return true;
}

console.dir(myFunction.prototype);
console.dir(myFunction.prototype.constructor);

  1. myFunction() 이라는 함수를 생성했다. 함수가 생성됨과 동시에 myFunction() 함수의 prototype 프로퍼티에는 이 함수와 연결된 프로토타입 객체가 생성된다.
  2. myFunction.prototype은 myFunction() 함수의 프로토타입 객체를 의미한다. constructor, __proto__ 라는 두 개의 프로퍼티를 가진다.
    이 객체는 myFunction() 함수의 프로토타입 객체이므로 constructor 프로퍼티가 있다.
    이 객체 역시 자바스크립트 객체이므로, 예외 없이 자신의 부모 역할을 하는 __proto__ 가 있다.
  3. myFunction.prototype.constructor 의 결과 값을 보면, myFunction() 함수를 가리키고 있다.

함수 객체와 프로토타입 객체는 prototype 과 constructor 라는 프로퍼티를 통해 서로를 참조하는 관계이다.

 

프로토타입의 두 가지 의미

자바스크립트는 다른 언어와는 다르게 프로토타입 기반의 객체지향 프로그래밍을 지원한다.

객체지향 프로그래밍에서는 클래스를 정의하고 이를 통해 객체를 생성하지만, 자바스크립트에는 이런 개념이 없다.대신에 객체 리터럴이나 생성자 함수로 객체를 생성한다.

 

이렇게 생성된 객체의 부모 객체가 바로 '프로토타입' 객체이다.

모든 객체에는 자신의 부모인 프로토타입 객체를 가리키는 참조 링크 형태의 숨겨진 프로퍼티([[Prototype]] 프로퍼티)가 있는데, 이러한 링크를 암묵적 프로토타입 링크(Implicit prototype link) 라고 부른다.

 

주의할 점은, 암묵적 프로토타입 링크와 prototype 프로퍼티를 구분해야 한다.

모든 객체는
자신을 생성한 생성자 함수의 prototype 프로퍼티가 가리키는 프로토타입 객체를
자신의 부모 객체로 설정하는 암묵적 프로토타입 링크로 연결한다.

예제를 살펴보자.

function Person(name) {
    this.name = name;
};

var foo = new Person('foo');

console.dir(Person);
console.dir(foo);

 

Person() 생성자 함수로 생성된 foo 객체는 Person() 함수의 프로토타입 객체를 암묵적 프로토타입 링크로 연결한다.

따라서
Person 함수 객체의 prototype 프로퍼티 = foo 객체의 __proto__ 프로퍼티
가 성립한다.

prototype 프로퍼티는 함수 입장에서 자신과 링크된 프로토타입 객체를 가리키는 것이고,
__proto__(암묵적 프로토타입 링크) 프로퍼티는 객체의 입장에서 자신의 부모 객체인 프로토타입 객체를 가리키는 것이다.

 

프로토타입 체이닝

객체는 자기 자신의 프로퍼티 뿐만 아니라, 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티 또한 자신의 것처럼 접근하는 것이 가능하다.

이것을 가능하게 하는 것이 바로 프로토타입 체이닝이다.

자바스크립트에서는 특정 객체의 프로퍼티나 메서드에 접근하려고 할 때,
해당 객체에 접근하려는 프로퍼티 또는 메서드가 없으면
암묵적 프로토타입 링크를 따라 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티를 차례대로 검색한다.

이것을 프로토타입 체이닝이라고 한다.

객체 리터럴 프로토타입 체이닝

리터럴로 생성된 객체는 내부적으로 Object() 함수 객체를 통해 생성된다.

Object() 함수는 암묵적 프로토타입 링크로 Object.prototype 객체를 가리킨다.

 

이에 따라 어떻게 프로토타입 체이닝이 이뤄지는지 예제를 살펴보자.

var myObject = {
    name: 'foo',
    sayName: function() {
        console.log('My Name is ' + this.name);
    }
}

myObject.sayName();
console.log(myObject.hasOwnProperty('name'));
console.log(myObject.hasOwnProperty('nickName'));
myObject.sayNickName();

왜 myObject 객체가 hasOwnProperty() 메서드를 호출할 때는 에러가 발생하지 않았을까?

 

객체 리터럴로 생성한 객체는 Object()라는 내장 생성자 함수로 생성된 것이다.

Object() 생성자힘수도힘수객체이므로prototype이라는프로퍼티 속성이 있다.

 

따라서 Object() 함수의 prototype 프로퍼티가 가리키는 Object.prototype 객체를 자신의 프로토타입 객체로 연결한다.

 

  1. myObject.sayName(); : 해당 객체 내에 메서드가 있어서 바로 수행된다.

  2. myObject.hasOwnProperty('name') : myObject 내에는 hasOwnProperty() 라는 메서드가 없다.
    따라서 암묵적 프로토타입 링크를 따라, 그것의 부모 역할을 하는 Object.prototpye 프로토타입 객체에서 hasOwnProperty() 메서드가 있는지 검색한다.
    hasOwnProperty() 메서드는 자바스크립트 표준 API 로 Object.prototype 객체에 포함되어 있다.
    따라서 코드가 정상적으로 수행된다.

생성자 함수로 객체를 생성하는 경우의 프로토타입 체이닝

생성자 함수로 객체를 생성하는 경우는 객체 리터럴 방식과 약간 다른 프로토타입 체이닝이 이뤄진다.

예제를 살펴보자.

function Person(name, age, hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
};

var foo = new Person('foo', 30, 'tennis');

console.log(foo.hasOwnProperty('name')); // true
console.dir(Person.prototype);

foo.hasOwnProperty('name') 를 실행하면 먼저 foo 객체에서 hasOwnProperty() 메서드를 찾는다.

 

foo 객체에는 없으므로, 암묵적 프로토타입 링크를 통해 부모 객체인 Person.prototype 에서 hasOwnProperty() 메서드를 찾는다.

 

Person.prototype 에도 없으므로, Person.prototype 의 프로토타입 객체에서 찾는다. Person.prototype 객체도 역시 자바스크립트 객체이므로 Object.prototype 을 프로토타입 객체로 가진다. 따라서 Object.prototype 에서 hasOwnProperty() 메서드를 찾는다.

 

Object.prototype 객체의 hasOwnProperty() 메서드를 실행한다.

 

위의 예제들을 통해 알 수 있듯이 Object.prototype 객체프로토타입 체이닝의 종점이다.

 

이와 같은 방식으로 자바스크립트의 숫자, 문자열, 배열 등에서 사용되는 표준 메서드들의 경우, Number.prototype, String.prototype, Array.prototype 등에 정의되어 있다.

 

프로토타입 체이닝 활용하기

빌트인 프로토타입에 메서드 추가하기

자바스크립트는 Object.prototype, String.prototype 등과 같이 표준 빌트인 프로토타입 객체에도 사용자가 직접 정의한 메서드들을 추가하는 것을 허용한다.

Object.prototype.testMethod = function() {
    console.log('test method');
}

프로토타입 객체에 동적으로 프로퍼티 추가/삭제하기

프로토타입 객체 역시 자바스크립트 객체이므로 일반 객체처럼 동적으로 프로퍼티를 추가/삭제하는 것이 가능하다.

그리고 이렇게 변경된 프로퍼티는 실시간으로 프로토타입 체이닝에 반영된다.

function Person(name) {
    this.name = name;
};

var foo = new Person('foo');

foo.sayHello(); // Uncaught TypeError: foo.sayHello is not a function

Person.prototype.sayHello = function() {
    console.log('Hello');
};

foo.sayHello(); // Hello

프로토타입 객체의 this 바인딩

프로토타입 객체는 메서드를 가질 수 있다.

만약 프로토타입 메서드 내부에서 this 를 사용한다면 이는 어디에 바인딩 될까?

 

객체의 메서드 호출 시 this 바인딩 규칙이 그대로 적용된다.

즉, 메서드 호출 패턴대로, this는 그 메서드를 호출한 객체에 바인딩된다.

function Person(name) {
    this.name = name;
};

var foo = new Person('foo');

Person.prototype.getName = function() {
    return this.name;
};

Person.prototype.name = 'person';

console.log(foo.getName()); // foo

console.log(Person.prototype.getName()); // person

foo.getName() 실행 시 getName() 메서드는 foo 객체에서 찾을 수 없으므로, 프로토타입 체이닝이 발생한다.
결과적으로 Person.prototype의 getName() 이 실행된다.
그런데 getName() 메서드를 호출한 객체는 foo 객체이므로, this 는 foo 객체에 바인딩이 된다.

 

Person.prototype.getName() 실행 시 Person.prototype 객체의 getName() 이 실행된다.
getName()을 호출한 객체는 Person.prototype 이므로 this는 Person.prototype 객체에 바인딩된다.

 

디폴트 프로토타입 객체를 다른 객체로 변경하기

디폴트 프로토타입 객체는 함수가 생성될 때 같이 생성되는 프로토타입 객체를 말한다.
이 객체를 다른 객체로 변경하는 것이 가능하다.

이러한 특징을 이용해서 객체지향의 상속을 구현하는 것이다.

 

여기서 주의할 점이 있다.

생성자 함수의 프로토타입 객체가 변경되면, 변경된 시점 이후에 생성된 객체들은 변경된 프로토타입 객체로 암묵적 프로토타입 링크가 이뤄진다.

 

반면, 생성자 함수의 프로토타입이 변경되기 이전에 생성된 객체들은 기존 프로토타입 객체로의 링크를 유지한다.

function Person(name) {
    this.name = name;
};

console.log(Person.prototype.constructor); // Person 함수 객체

var foo = new Person('foo');

console.log(foo.country); // undefined

Person.prototype = {
    country: 'korea'
};

console.log(Person.prototype.constructor); // Object 함수 객체

var bar = new Person('bar');

console.log(foo.country); // undefined
console.log(bar.country); // korea

console.log(foo.constructor); // Person 함수 객체
console.log(bar.constructor); // Object 함수 객체

Person.prototype 을 리터럴 객체로 변경한 후, Person.prototype.constructor 값이 Object() 생성자 함수로 출력된 것을 주목하자.

 

변경한 프로토타입 객체는 리터럴로 생성하여 단지 country 프로퍼티만 있다. 즉, 디폴트 프로토타입 객체처럼 constructor 프로퍼티가 없다.

 

이 경우에도 암묵적 프로토타입 링크는 존재하기 때문에 프로토타입 체이닝은 발생한다.

 

모든 자바스크립트의 리터럴 객체는 프로토타입 객체로 Object.prototype 을 가진다. 따라서 Object.prototype 의 constructor 가 참조하고 있는 Object() 함수를 연결하여, Object() 생성자 함수가 출력되는 것이다.

 

 

+ Recent posts