- Published on
[언어학][JS] Prototype
- Authors
- Name
- Kim, Dong-Wook
Table of Contents
개론
현업에 있다보면 항상 신기하게도, 내가 사용하는 도구의 본질에서 멀어집니다.
본질보다는 누군가가 쌓아놓은 Framework나 Layer 위에서 움직입니다. 이걸 추상화라고도 합니다.
매일 같이 Javascript(이하 JS)를 사용하고 있지만, 일을 하다보면 추상화해놓은 것들을 조합만 해도 대부분의 요구사항을 만들 수 있습니다
따라서 이게 어떻게 돌아가는지에 대한 것
은 자연스럽게 관심에서 멀어집니다. 하지만 그런 How를 알아야, 기존의 Framework에 한정되지 않고, 새로운 것들을 만들어나갈 수 있습니다.
그래서 오늘은 그런 어떻게 돌아가는지에 대한 것
중에 하나인 JS Prototype에 대해 알아보고, 독자분들께 영감이 되었으면 하는 바램으로 글을 작성했습니다.
글이 조금 깁니다. 또한 자바스크립트는 왜 프로토타입을 선택했을까 해당 글을 참조했습니다.
Prototype
결론부터 말하자면 Prototype은 문맥을 존중하는 기술의 구현체
입니다.
왜 Prototype인가
컴퓨터 공학과 학생들이 프로그래밍을 배우면 대부분 C, C++, Python, Java 같은 언어로 입문을 합니다. 저 또한 마찬가지 였습니다.
그러다 Javascript 언어를 처음 접하면 가장 생소한게 바로 Prototype입니다.
대부분 왜 Prototype이라는게 있어요?
라고 하면 상속을 지원하기 위해서요
라는 답변이 돌아오고, 저 같이 Class 문법에 익숙했던 사람은 그냥 Class 쓰면 되는거 아닌가? 라는 생각이 머리속에 듭니다.
그러나 그때 뿐, Javascript가 Class문법을 지원한다는 사실을 알고 나면 Prototype에 대한 의문은 저 멀리 사라져있습니다.
그런데 Prototype을 차용했기 때문에 Javascript만이 가졌던 특성, 그리고 그런 특성들로 인해 있었던 특성이 있습니다.
바로 bind, call, apply, hoisting, 그리고 악명높은 this이죠.
왜 JS는 위에 나열한 이상한 특성들을 가지면서 또한 정말 특이하다 할 수 있는 Prototype을 선택한 걸까요?
바라보는 관점 바꾸기
Class 기반 OOP 언어가 객체를 바라보는 관점을 생각해봅시다.
이런 언어는 서양 철학에 기반하고 있습니다. 바로 눈앞에 실제로, 구체적으로 존재하는 사물이 있다면 반드시 그것의 본질이 존재한다
는 플라톤의 철학이죠.
우리가 앉는 의자를 예로 들겠습니다.
"원목 의자", "바퀴 달린 의자", "하이 체어" 등등. 이런 수많은 의자가 실제로 존재한다면 그 본질에는 "의자"라는 것이 존재한다.
이러한 본질의 세계를 Idea라고 하며
,현실의 의자는 이런 Idea의 "의자"를 모방한 것
이다
이런 사고가 프로그래밍 언어에도 녹아들어 만들어진 언어가 바로 클래스 기반 객체지향 프로그래밍 언어
입니다.
그래서 본질이 되는 의자(Idea)를 만들고 본질을 상속받아 (원목)의자(Idea + a)가 되고 , 그걸 현실세계에 존재(인스턴스화)하도록 합니다.
Classification(분류)
이러한 플라톤의 이데아 이론은 후에 확장되어 분류(classification)
이란 개념으로 정립되었고 다음과 같이 분류를 정리했습니다
개체의 속성이 동일한 경우
개체 그룹이 같은 범주에 속한다. 범주는 정의와 구별의 합이다
이는 우리가 아는 전통적인 Class 기반 객체 지향 프로그래밍의 아이디어-일반화와 일치하며, 속성은 Class의 Property입니다. Property가 유사한 객체가 있다면 일반화 과정을 통해 클래스로 추상화됩니다.
Prototype
Prototype은 이런 Classification에 대해 정면으로 반박하여 나온 이론입니다.
철학자 비트겐슈타인은 다음과 같이 말했습니다
공유 속성의 관점에서 정의하기 어려운 개념이 있다(사실상 올바른 분류란 없다)
게임을 예로 들면서 해당 이야기를 생각해봅시다
일반적으로 게임은 승자와 패자가 명확합니다. 하지만 그렇지 않은 게임도 있습니다
- 승리 / 패배? 승자가 없는 게임도 있습니다(ex. ring around a rosy 또는 관점에 따라 승자가 달라질 수 있음)
- 숙련도 여부? 주사위 위주 게임에는 없습니다
즉 공통 속성(Property)를 정의할 수 없는 것들이 현실세계에는 많습니다. 게임 외에도 예술작품 또한 공통 속성을 정의할 수 없습니다.
세계에 미리 내재되어서 대상과 언어를 완전히 규정하는 어떤 언어란 존재하지 않는다 — 비트겐슈타인
사실 Class 기반 OOP 언어로 프로그래밍 언어들은 적절한 패턴(SOLID, SRP, 디자인 패턴 등)을 사용하지 않으면, 나중에 상속관계로 인해서 확장가능성이 떨어지는 경우를 많이 봤습니다.
처음부터 모든 것을 예측하여 완벽한 디자인이 나오지 않는 이상, 오히려 Class 기반으로 만들어진 프로그램은 시간이 지날수록 기능추가에 병목이 점차 커지는 현상을 목격한 경우가 많았습니다.
현실세계는 시간에 의존성이 있습니다. 따라서 처음부터 완벽한 프로그램이라는게 가능할까요?
이러한 Classification에 대해 철학자 비트겐슈타인은 다음과 같이 말했습니다
표현은 삶의 흐름 속에서만 의미를 갖는다 — 비트겐슈타인
문맥 따르기
표현은 삶의 흐름 속에서만 의미를 갖는다
라는 말의 의미는 즉 진정한 본래의 의미
란 존재하지 않고 상황과 맥락에 의해서 결정된다
입니다.
친숙한 예를 들어봅시다
한국인이 "응"이라고 대답합니다. "응"이 가지는 문법적 의미는 "긍정"입니다.
하지만 실제로는 맥락(Context)에 따라 의미가 바뀝니다.
"응"이라고 해도 상황, 어조 등에 따라 부정의 의미가 될 수도 있는 것이죠.
또한 현실 세계에서는 대상을 분류할 때 속성 보다는 유사성 을 통해 분류하게 된다고 합니다.
위의 그림을 봅시다.
위 그림처럼 가족이 있을 때 이 가족이 모두 공유하는 공통 속성은 없습니다. 어느 정도의 공통적인 특징을 있을 수 있지만 모든 가족 구성원에게 적용되는 공통된 특성은 없을 수 있습니다.
그렇지만 위의 그림을 보면 우리는 가족 사진
으로 분류합니다. 이런 분류 방식을 유사성
에 의한 분류라고 합니다.
이 이론은 Prototype 이론의 근거가 됩니다.
Prototype Theory
자바스크립트는 왜 프로토타입을 선택했을까의 말을 그대로 인용하겠습니다.
"인간은 등급이 매겨진 (개념) 구조(graded structure)를 가진다"라고 주장합니다. 인간은 사물을 분류할 때 자연스럽게 가장 유사성 높은 것 순서대로 등급을 매긴다는 의미로 볼 수 있습니다. 이렇게 분류했을 때 가장 높은 등급을 가진 녀석이 나올 텐데요, 이것이 바로 원형(Prototype)이다.
란 주장이 프로토타입 이론입니다.
‘타조'는 전통적인 분류에선 같은 새가 되지만 프로토타입 이론에서는 ‘원형'에서 가장 멀리 떨어진, 즉 ‘비전형적인' 녀석이 됩니다. 범주의 가장 끄트머리에 있는 녀석이 되는 거죠.
즉, 객체는 ‘정의’로부터 분류되는 것이 아니라 가장 좋은 보기(prototype, exemplar)로부터 범주화된다고 합니다.
같은 단어라 할지라도 누가 어떤 상황(context)에서 접했나에 따라 의미가 달라진다는 것입니다. (의미사용이론)
예를 들면 아이가 생각하는 새의 범주에서 ‘참새’는 명확하게 새에 속하지만 ‘펭귄' 은 해당 범주에 속하지 못할 수도 있습니다. 아이가 생각할 땐 펭귄이 매우 비전형적이기 때문이죠. 하지만 조류학자가 생각할 때 ‘참새'와 ‘펭귄'은 명확하게 유사한 새의 범주에 속할 수 있습니다. 같은 단어여도 어떤 상황(누가, 어디서…)에서 접했나에 따라 범주는 크게 달라집니다.
즉 정리하자면 다음과 같습니다
- 현실에 존재하는 것 중 가장 좋은 본보기를 원형(prototype)으로 선택한다.
- 문맥(컨텍스트)에 따라 ‘범주’, 즉 ‘의미’가 달라진다.
Prototype is
이런 이론에 입각하여 태어난 Self언어와 가족유사성을 언어 차원에서 구현한 언어들에 영감을 받은 언어가 Javascript입니다.
Prototype 기반의 OOP 언어의 특징은 다음과 같습니다
- Instance 수준에서 메소드와 변수를 추가
- 객체 생성은 일반적으로 복사를 통해 달성
- 확장(extends)은 클래스가 아닌 위임(delegation)
- 현재 객체가 메세지에 반응하지 못할 때, 다른 객체로 메세지를 전달하여 상속의 본질을 지원
- 개별 객체 수준에서 객체를 수정하고 발전시키는 능력은 선험적 분류의 필요성을 줄이고 반복적인 프로그래밍 및 디자인 스타일을 장려
- Classification 하지 않고 Similarity를 활용
- 설계는 맥락에 의해 평가
다시 한 번 정리하면 다음과 같습니다
- Classification를 우선하지 않는다.
생성된
객체 위주로 유사성을 정의한다 - 어휘, 쓰임새는 Context에 의해 평가된다
- 실행 컨텍스트, 스코프 체인이 여기에서 파생되었습니다
- Closure, this, hoisting 등등이 모두 프로토타입의 Context를 표현하기 위한 것 입니다.
코드를 통해 다시 한 번 살펴봅시다
Javascript - Prototype
function 참새(){
this.날개갯수 = 2;
this.날수있나 = true;
}
const 참새1 = new 참새();
console.log("참새의 날개 갯수 : ", 참새1.날개갯수); // 2
// 닭
function 닭(){
this.벼슬 = true;
}
닭.prototype = 참새1; // reference(오른쪽이 인스턴스인 점 주목)
const 닭1 = new 닭();
console.log("닭1 날개 : ", 닭1.날개갯수, ", 날수있나? ", 닭1.날수있나); // 2, true
닭1.날수있나 = false;
console.log("다시 물어본다. 닭1은 날 수 있나? :", 닭1.날수있나); // false
// 펭귄
// 아래는 고전적인 방식의 프로토타입 연결
function 펭귄(){
참새.call(this); // copy properties
}
펭귄.prototype = Object.create(참새.prototype); // 프로토타입 연결
const 펭귄1 = new 펭귄();
console.log("펭귄1 날개 : ", 펭귄1.날개갯수, ", 날수있나? ", 펭귄1.날수있나); // 2, true
펭귄1.날수있나 = false;
console.log("다시 물어본다. 펭귄1은 날 수 있나? :", 펭귄1.날수있나); // false
주목해야할 부분은 다음과 같습니다
- 닭의 prototype을 주입할 때, 참새(함수)가 아닌 참새1(인스턴스)를 주입합니다. 프로토타입 이론은 이미 존재하는 사물을 통해 범주화한다는 점에서 일치합니다
- 닭1은 날 수 없습니다. 닭1은 날 수 없어도 프로토타입에 해당하는 참새1은 날 수 있습니다. 같은 속성을 변경해도 프로토타입 객체의 속성은 변경되지 않습니다.
- 프로토타입에선 객체 생성(Object.create 함수)를 통해 확장된다는 부분이 좀 더 직관적으로 다가옵니다
- 닭1의 Prototype은 참새1이다
- 닭1에 없는 속성(날개개수)은 프로토타입 체인을 통해 참조된다
- 닭1에 동일한 속성명(날수있나)을 추가해도 원형은 변하지 않는다(위임)
- 원리적으로는 닭1을 통해 원형을 변경하는건 불가능해야 합니다. 다만 JS에선 문법적으로 가능합니다.(권장하지 않음)
Javascript - Lexical Scope
의미사용이론에 따르면 단어의 의미는 어휘적인, 근처 환경에서의 의미가 됩니다. 이는 Javascript에서 다음처럼 적용됩니다
변수의 의미는 어휘적인(Lexical), 실행 문맥(Execution Context)에서의 의미가 된다
그렇기에 Javascript가 동일 범위(Lexical)의 모든 선언
을 참고(Hoisting)해 의미를 정의합니다.
프로토타입 기반 언어인 Javascript에서는 단어의 의미가 사용되는 근처 환경
에서의 근처
를 어휘적인 범위(Lexical Scope)로 정의했습니다. Javascript Engine은 코드가 로드될 때 실행 컨텍스트를 생성하고 그 안에 선언된 변수, 함수를 실행 컨텐스트 최상단으로 Hoisting합니다. 이러한 범위를 렉시컬 스코프라 합니다.
예제 코드는 다음과 같습니다
// 전역 실행문맥 생성. 전체 정의(name, init) 호이스팅
var name = 'Kai';
init(); // init 실행문맥 생성. 내부 정의(name, displayName) 호이스팅
function init() {
var name = "Steve";
function displayName() {
console.log(name); // 현재 실행문맥 내에 정의된게 없으니 outer 로 chain
// var name = 'troll?'; // 주석 해제되면 호이스팅
}
displayName(); // displayName 실행문맥 생성. 내부 정의 호이스팅.
}
코드를 로드하면 다음과 같은 Context가 생깁니다
- Global Execution // 1
- Lexical : name, init
- Execution : init // 2
- Lexical : name, displayName
- Outer : global
- Execution : displayName // 3
- Lexical : null
- Outer : init
사진으로 정리하면 다음과 같습니다
그래서 정리하면 왜 실행 문맥
, 렉시컬 스코프
, 호이스팅
이 존재할까요?
프로토타입 철학의 근원인 비트겐슈타인류에서 가장 중요하게 생각하는 것이
어휘
이고 이것은문맥
내에서만 의미를 가집니다
이런 핵심을 구현하기 위해서 자연스럽게 발생한 특징입니다.
Javascript - this
그 악명높은 this 입니다. Class 기반 객체지향언어에서의 this와 동작이 매우 다르기 때문에 대부분 대충 넘어가는 this입니다.
자바스크립트는 왜 프로토타입을 선택했을까 를 읽으면서 왜 this가 이렇게 동작해야하는지 알게되어서 매우 행복합니다.
다음은 this에 대한 오해입니다
- this 는 기본적으로 window 다 ( X )
- 이벤트 리스너에서 등록한 콜백의 this 는 내부에서 bind 등을 통해 바뀌기때문에 무엇인지 알 수 없다. ( X )
- this 는 외워야한다 ( X )
이러한 오해를 바로잡기 위해선 먼저 프로토타입 철학에서 이런 상황을 어떻게 해석하는지 알 필요가 있습니다.
이전에 [문맥 따르기] 기억나시나요? 응
이라는 말이 상황에 따라 긍정 또는 부정의 의미를 가졌습니다.
비트겐슈타인은 그의 대표적인 저서 철학적 탐구
에서 단어의 쓰임새가 곧 의미라는 점을 강조했습니다(의미사용이론
). 그는 이를 발화
라고 얘기했는데요, 위에서 예시 들었던 응
이라는 대답은, 어디서 발화
되느냐에 따라서 단어의 의미가 달라집니다. 즉 받아들이는 대상
에 따라서 같은 단어도 의미가 달라진다는 얘기입니다.
이 점이 Prototype과 Class의 대표적인 차이입니다. 미리 분류된 Class를 중요하게 여기는 전통적인 방식과 달리, Prototype에서는 받아들이는 주체와 문맥이 가장 중요한 것 입니다. 프로그래밍으로 보자면 실행(invoke)하는 객체
가 중요하다는 의미입니다.
이 점이 Prototype 기반 언어인 Javascript에서의 this가 다른 Class 기반 언어에서의 this와 다르게 동작하는 이유입니다.
Prototype 기반 언어에서는 this가 정의된 함수가 어떻게 발화(invoke) 되었는지에 따라 가리키는 값이 달라집니다. 정확히는 받아들이는 대상의 컨텍스트를 가리킵니다.
이를 이해하려면 먼저 메소드와 메시지를 명확하게 알아야 합니다
- 메소드 : 객체의 함수
- 메시지 : 메소드를 실행하라는 메시지 전달
Java에서는 클래스의 메소드를 호출하는 행위를 메시지라 합니다. Javascript를 예로 들면 foo 라는 객체가 있고 그 내부에 bar()라는 함수가 있을 때, 다음처럼 발화(invoke)할 객체를 지정할 수 있습니다.
- foo.bar()
- bar.call(foo)
- var boundBar = bar.bind(foo)
위처럼 foo 객체를 통해 발화한 함수는 내부 this가 무조건 foo를 가리킵니다. 만약 아무것도 지정되어있지 않으면 Global(브라우저라면 window)을 가리킵니다.
예제를 봅시다
var someValue = 'hello';
function outerFunc() {
console.log(this.someValue); // 첫번째 : ?, 두번째 : ?
this.innerFunc();
}
const obj = {
someValue : 'world',
outerFunc,
innerFunc : function() {
console.log("innerFunc's this : ", this); // 첫번째 : ?, 두번째 : ?
}
}
obj.outerFunc(); // 'world'
outerFunc(); // 'hello'
- obj.outerFunc()를 실행하면 world가, outerFunc()를 실행하면 hello가 나옵니다
- outerFunc를 invoke한 주체가 누군지 알면 this를 결정할 수 있습니다
obj.outerFunc()의 호출 상황을 그림으로 그려보면 다음과 같습니다
- Javascript Engine이 코드를 실행합니다.(브라우저에서 use strict 모드가 아닌 경우 this는 window를 가리킵니다)
- obj.outerFunc() 코드를 보면, Engine은 obj에 outerFunc를 실행하라는 메시지를 보냅니다
- obj에서 outerFunc를 invoke 합니다. 코드 로드 시 만들어져있는 실행문맥을 참고해 실행합니다. 이때 실행 문맥상의 this는 발화한 obj를 가리킵니다.
- 실행 중에 this.intterFunc를 만납니다. 엔진은 this가 가리키는 obj에 innterFunc를 실행하라는 메시지를 보냅니다
- obj에 innterFunc이 선언되어있으니 잘 실행합니다
outerFunc 호출 상황을 그림으로 그려보면 다음과 같습니다
- JS Engine은 자신(global)의 실행문맥상에 존재하는 outerFunc을 호출합니다
- 발화한 지점이 Engine(global)이니 this는 엔진을 가리킵니다. 엔진에 innerFunc을 실행하라는 메시지를 보냅니다
- 글로벌 실행문맥에는 innerFunc이 없기 때문에 에러가 납니다
즉 발화가 어느 지점에서 일어났는가, 일어난 시점에서 Context는 어떻게 되었는가가 핵심입니다.
add event listener 예제를 봅시다
function handle() {
console.log(this); // 첫번째 ?, 두번째 ?
}
document
.getElementsByTagName('body')[0]
.addEventListener('click', handle); // 첫번째. 호출되었다고 가정.handle();
handle(); // 두번째. 첫번째 이후에 호출되었다고 가정.
addEventListener 함수는 해당 element에 callback을 등록하는 함수입니다. "div"객체에 'handle'메소드를 등록했다고 볼 수 있습니다.
- 브라우저에서 div를 클릭하면 Engine이 반응합니다
- 해당 div에 등록된 event listener들을 실행하라는 메세지를 보냅니다
- div element에서 handle을 발화합니다
- 이때 handle의 실행문맥의 this는 발화한 주체(div element)가 됩니다
So.. Prototype is
Prototype은 문맥을 존중하는 기술
입니다.
Prototype은 Class의 다른 구현이 아닌, 완전히 새로운 인식 하에 만들어진 이론입니다.
JS에서는 Class라는 Syntax Sugar가 있을 뿐이지, 내부적으로는 아예 다른 관점으로 객체를 바라보고 있습니다. 따라서 다른 Class기반 언어와 달리 JS의 Prototype, Hoisting, this가 왜
존재하고 이해할 수 있습니다.
글을 쓴 이유(feat. BuckleScript a.k.a Rescript)
오늘 이 글을 쓰게 된 이유는 Flow와 Typescript(이하 TS)를 비교하다가 문득 Javascript 자체에 의문이 들어서 쓰게되었습니다.
제 첫 회사는 Ocaml 기반의 BuckleScript(지금은 Recript로 리브렌딩)를 Frontend 언어로 사용하던 회사였습니다. 구조조정으로 인해 3개월만에 나오긴 했지만 제 프로그래밍 실력에 가장 큰 영향을 줬던 회사였습니다.
그 때 사용했던 BuckleScript라는 언어가 가진 철학, 패러다임, 함수 등이 제게 큰 영감을 주었고, 그런 영감이 저를 어떤 방향으로 이끌었거든요.
하지만 항상 이런 의문이 있었습니다. 왜 BuckleScript 같은 마이너 언어를 선택하게 되었을까?
. 회사라는게 우선 돈을 벌어야하기 때문에 메인스트림에서 아예 벗어나는게 불가능하거든요.
그 이유를 Flow와 TS의 역사를 알아보면서 깨닫게 되었습니다.
알아보기에 앞서 우선, Flow는 Facebook이 TS는 Microsoft에서 만든 JS타입 추론기(혹은 시뮬레이터) 입니다. 비슷해보이지만 두 추론기는 지향하는바가 다릅니다.
TS는 AST를, Flow는 Graph를 사용합니다. 좀 더 간단히 이야기하자면, Typescript는 타입을 즉시 결정하고, Flow는 타입 결정을 추론하여 결정합니다.(그래서 Flow는 타입 정보를 TS에 비해 덜 적어도 됩니다. 물론 이것도 최근에는 TS 옵션이 많이 좋아져서 꼭 그렇지도 않습니다.)
Flow의 경우 Ocaml 언어를 통해 Javascript Code Parsing and Analyze 로직이 짜여져있습니다. Ocaml의 힌들리 밀러 타입 시스템이 Flow의 타입 결정 로직과 일치하기 때문에 매우 똑똑한 결정이었을 것 입니다.
하지만 JS는 Ocaml과 달리 함수형 패러다임에 입각한 언어가 아니기 때문에 타입 추론 버그에 시달릴 수 밖에 없었습니다. 객체에 대한 mutable한 접근을 시뮬레이팅 하거나, JS의 특정한 문법을 Flow의 Graph 전파 로직 내에서 예외처리하기 어려웠겠죠(ES5의 문법적 한계 때문이 큽니다.)
또한 점진적 Migration 측면에서 Typescript가 더 훌륭했기 때문에 점차 시장에서 밀렸을 것 입니다.
그렇지만 Flow가 가진 매력적인 요소들이 많았기에, 이런 요소들을 실제 개발에 도입할 수 있는 환경(Buckle Script or Rescript)을 구축하려고 노력했고, 그 노력이 그린랩스라는 회사의 탄생으로 이어진게 아닌가 싶었습니다.
더 자세한건 Flow와 Typescript의 채택 글을 참고해주세요
그래서 정리하면 이렇습니다
- Flow는 페이스북이 만들었고 Ocaml 언어로 만들어졌다
- React 또한 Ocaml로 프로토타이핑 되었던 시절이 있다
- BuckleScript는 사실상 Ocaml을 기반으로한 Readable Javascript Converter이다
- Flow는 Javascript 언어의 한계로 버그 및 이슈가 많다(단, ES6가 되면서 많이 개선되었다)
따라서 BuckleScript를 기반으로 Flow의 장점을 차용해서 Javascript 개발을 하자
와 같이 된게 아닌가 싶었습니다.
정확하진 않겠지만 Flow를 분석하다 보니 예전부터 의문으로 생각하던게 하나 해결된 것 같아 후련합니다.
후기
최근 본질
에 대한 글을 쓰고 있습니다.
해당 글도 본질
에 대해 의문을 추구하다가, 우연히 자바스크립트는 왜 프로토타입을 선택했을까 라는 글을 발견해서 썼습니다.
썼다기 보다는 거의 배꼈네요.
그냥 Prototype, Hoisting, this는 그런거야
라고 생각하고 넘어갔는데, 비트겐슈타인의 철학과 아리스토텔레스의 분류학까지 갈 줄 몰랐습니다.
하지만 그 본질에 기저한 것들을 알게 되니, 명확해지고 행복합니다.
Inspired By
긴 글 읽어주셔서 감사합니다.