반응형
사내 스터디로 진행되는 "모던 자바 스크립트 Deep Dive" 리딩 관련 정리입니다
깊이 있는 프론트 지식 공유보단 기본적으로 알아야할 지식 중심으로 키워드와 라이프싸이클을 중심으로 기술하려고합니다
http://www.yes24.com/Product/Goods/92742567
13장. 스코프
- 스코프
- 식별자가 유효한 범위를 나타낸다
- 모든 식별자(변수 이름, 함수 이름, 클래스 이름 등등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다
- 식별자 결정
- 자바 스크립트 엔진은 이름이 같은 두 개의 변수 중에서 어떤 변수를 참조해야 할 것인지를 결정하는데 이를 식별자 결정이라 한다
- 식별자는 어떤 값을 구별할 수 있어야 하므로 유일 해야한다
- 식별자인 변수 이름은 중복될 수 없다, 즉 하나의 값은 유일한 식별자에 연결되어야한다 ( 같은 스코프 내에서 )
- 스코프는 네임스페이스
// 13-1 var x = 'global'; function foo() { var x = 'local'; console.log(x); // 'local' } foo(); console.log(x); // 'global'
- ※ var 키워드의 예외적인 중복 선언 가능
// 13-2 function foo() { var x = 1; var x = 2; // var 키워드로 선언된 변수는 같은 스코프내에서 중복 선언을 허용한다 // 자바 스크립트 엔진을 통해 x = 2; 처럼 동작 console.log(x); // 2 } foo(); function bar() { let x = 1; let x = 2; // SyntaxError: Identifier 'x' has already been declared } bar();
- 스코프의 종류
- 전역 스코프, 지역 스코프로 나뉜다
// 13-3 var x = "global x"; var y = "global y"; function outer() { var z = "outer's local z"; console.log(x); // 1.global x console.log(y); // 2.global y console.log(z); // 3.outer's local z function inner() { var x = "inner's local x"; console.log(x); // 4.inner's local x console.log(y); // 5.global y console.log(z); // 6.outer's local z } inner(); } outer(); console.log(x); // global x console.log(z); // ReferenceError: z is not defined
- outer 함수 내부 범위와 inner 함수 내부의 {} 로 둘러쌓인 코드 블럭을 지역 스코프라고 한다
- 가장 밖의 var x, y 등을 전역 스코프라고 한다
- 전역 변수는 어디서든 참조 가능
- 지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효
- 전역 스코프, 지역 스코프로 나뉜다
- 스코프 체인
- 스코프가 함수의 중첩에 의해 계층적 구조를 갖는다 ( 스코프 체인 )
- 함수 몸체 내부에서 함수가 정의된 것을 함수의 중첩이라고 한다
- 함수 몸체 내부에서 정의한 함수를 중첩 함수, 중첩 함수를 포함하는 함수를 외부 함수라고 한다
- 변수를 참조할 때 자바 스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다 ( 스코프 체인에 의한 변수 검색 )
- 스코프 체인에 의한 변수 검색
- 상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만 하위 스코프에서 유효한 변수를 상위 스코프에서 참조할 수 없다
- 함수를 검색하는 방식도 동일하다
- 함수 레벨 스코프
- 대부분의 프로그래밍 언어는 함수 몸체만이 아니라 모든 코드 블록 (if, for, while 등)이 지역 스코프를 만든다
( 블록 레벨 스코프 ) - 하지만 var 키워드로 선언된 변수는 오로지 함수의 코드 블록(함수 몸체)만을 지역 스코프로 인정한다.
( 함수 레벨 스코프 )
var의 특성으로 if문 코드블록안에서 생성되었을지라도 전역변수이다 ( x = 10; 이랑 동일 )// 13-4 var x = 1; if (true) { var x = 10; } console.log(x); // 10
// 13-5 var i = 10; for (var i = 0; i < 5; i++) { console.log(i); // 0, 1, 2, 3, 4 } console.log(i); // 5 << 10을 기대했다면 var의 특성으로 인해 의도치않게 값이 변경됐다
- 대부분의 프로그래밍 언어는 함수 몸체만이 아니라 모든 코드 블록 (if, for, while 등)이 지역 스코프를 만든다
- 렉시컬 스코프
- 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정 ( 동적 스코프 )
- 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정 ( 렉시컬 스코프 )
- 자바 스크립트는 렉시컬 스코프를 따르므로 함수를 어디서 호출했는지가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정
- 함수가 호출된 위치는 상위 스코프 결정에 어떠한 영향도 주지않는다
// 13-6 var x = 1; function foo() { var x = 10; bar(); } function bar() { console.log(x); } foo(); // 1 bar(); // 1
14장. 전역 변수의 문제점
- 전역 변수의 무분별한 사용은 위험하다
- 지역 변수의 생명 주기는 함수의 생명 주기와 일치한다 ( 함수가 끝나면 변수도 소멸된다 )
- 변수의 생명 주기는 메모리 공간이 확보된 시점부터 메모리 공간이 해제되어 가용 메모리 풀에 반환되는 시점까지
- 일반적으로 함수가 종료하면 함수가 생성한 스코프도 소멸, 하지만 누군가 스코프를 참조하고 있다면 스코프는 해제되지않고 생존
- 호이스팅
- 스코프를 단위로 동작
- 변수 선언이 스코프의 선두로 끌어 올려진 것 처럼 동작하는 자바스크립트 고유의 특징
- 변수 선언이 끌어 올려지는 것, 초기화랑 같이 끌어올려지는게 아니다
// 14-1
var x = 'global';
function foo() {
console.log(x); // undefined
var x = 'local';
}
foo();
console.log(x); // 'global'
- 전역 변수의 생명주기
- 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체
- 코드의 마지막 문이 실행될때까지 유지된다
- var 키워드로 선언한 전역 변수의 생명 주기는 전역 객체의 생명 주기와 일치한다
- 전역 변수의 문제점
- 암묵적 결합
- 전역 변수를 선언한 의도는 전역, 즉 코드 어디서든 참조하고 할당할 수 있는 변수를 사용하겠다는 것
- 이는 모든 코드가 전역 변수를 참조하고 변경할 수 있는 암묵적 결합을 허용하는 것이다
- 변수의 유효 범위가 크면 클수록 코드의 가독성은 나빠지고, 의도치 않게 상태가 변경될 위험성도 높아진다
- 긴 생명주기
- 전역 변수는 생명 주기가 길다 ( 메모리 리소스도 오랜 기간 소비한다 )
- 지역 변수는 전역 변수보다 생명 주기가 훨씬 짧고, 메모리 리소스도 짧은 기간만 소비한다
- 스코프 체인 상에서 종점에 존재
- 전역 변수의 검색 속도가 가장 느리다 ( 큰 차이는 없지만, 느리긴 제일 느리다 )
- 네임 스페이스 오염
- 파일이 분리되어 있더라도, 하나의 전역 스코프를 공유한다
- 같은 이름을 쓸 경우 예상치 못한 결과를 가져올 수 있다 ( 같은 이름을 쓰는지 조차 잘 몰를 수도 있다 )
- 암묵적 결합
- 전역 변수의 사용을 억제하는 방법
- 반드시 전역 변수를 사용해야할 이유를 찾지 못한다면, 지역 변수를 사용하자
- 변수의 스코프는 좁을 수록 좋다
- 즉시 실행 함수
- 함수 정의와 동시에 호출되는 즉시 실행 함수는 단 한번만 호출
- 모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다
// 14-2 ( function () { var foo = 10; // 즉시 실행 함수의 지역 변수 // ... }()); console.log(foo) // ReferenceError: foo is not defined
- 네임 스페이스 객체
- 전역에 네임스페이스 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법
- 네임스페이스를 분리해서 식별자 충돌을 방지하는 효과는 있으나, 네임 스페이스 객체 자체가 전역변수에 할당되므로 크게 유용해보이진 않는다
- 모듈 패턴
- 클래스를 모방해서 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만든다
- 클로저를 기반으로 동작
- 전역 변수 억제 효과는 물론 캡슐화까지 구현 가능하다
- 클래스 외부에는 제한된 접근 권한을 제공하며, 원하지 않는 외부의 접근으로부터 내부를 보호하는 기능을 한다
// 14-3 var Counter = (function() { // private 변수 var num = 0; // 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환 return { increase() { return ++num; }, decrease() { return --num; } }()); console.log(Counter.num) // undefined, private 변수는 외부에서 접근 불가 console.log(Counter.increase()); // 1 console.log(Counter.increase()); // 2 console.log(Counter.decrease()); // 1 console.log(Counter.decrease()); // 0
15장. let, const 키워드와 블록 레벨 스코프
- var 키워드로 선언한 변수의 문제점
- 변수의 중복 선언 허용
- 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작
- 오직 함수의 코드 블록만을 지역 스코프로 인정
- 함수 외부에서 var 키워드로 선언된 변수는 코드 블록 내에서 선언해도 모두 전역 변수이다
- 변수 호이스팅
- var 키워드로 변수를 선언하면 변수 호이스팅에 의해 변수 선언문이 스코프의 선두로 끌어 올려진 것처럼 동작
- 할당문(초기화문) 이전에 변수를 참조하면 언제나 undefined가 된다
- 변수 선언문 이전에 변수를 참조하는 것은 에러를 발생시키진 않지만, 가독성을 떨어뜨리고 오류 발생 여지를 남긴다
- 변수의 중복 선언 허용
- let 키워드
- 변수 중복 선언 금지 ( 중복 선언시 SyntaxError 발생 )
- 블록 레벨 스코프
- 변수 호이스팅 ( 변수 선언문 이전에 참조하면 참조 에러가 발생 )
- let 키워드로 선언한 변수는 선언 단계와 초기화 단계가 분리되어 진행된다
- 스코프의 시작 지점부터 초기화 시작 지점까지 변수를 참조할 수 없는 구간을 일시적 사각지대라고 부른다
- let 키워드로 선언한 변수도 여전히 호이스팅이 발생한다
- 단 ES6에서 도입된 let, const, class를 사용한 선언문은 호이스팅이 발생하지 않는 것처럼 동작한다
// 15-1 let foo = 1; { console.log(foo); // ReferenceError: Cannot access 'foo' before initialization let foo = 2; }
- 전역 객체와 let
// 15-2 var x = 1; // 전역 변수 y = 2; // 암묵적 전역 function foo() {} // 전역 함수 let z = 3; console.log(window.x); // 1 console.log(x); // 1 console.log(window.y); // 2 console.log(y); // 2 console.log(window.foo); // f foo() {} console.log(foo); // f foo() {} console.log(window.z); // undefined console.log(z); // 3
- const 키워드
- const 키워드는 상수를 선언하기 위해 사용한다
- 선언과 초기화를 반드시 동시에 해야한다 ( SyntaxError )
- let 키워드와 마찬가지로 블록 레벨 스코프를 가지며, 변수 호이스팅이 발생하지 않는 것처럼 동작
- 재할당 금지
- const 키워드로 선언한 변수에 원시 값을 할당한 경우 변수 값을 변경할 수 없다 ( immutable value )
- const 키워드로 선언된 변수에 객체를 할당한 경우 값을 변경할 수 있다 ( 재할당만 금지, 불변은 아님 )
- 정리
- ES6를 사용한다면 var 키워드는 사용하지 않는다
- 재할당이 필요한 경우에 한정해 let 키워드를 사용, 변수의 스코프는 최대한 좁게 설정한다
- 읽기 전용으로 사용하는 원시 값과 객채에는 const 키워드를 사용한다, 보다 더 안전하게 사용할 수 있다
16장. 프로퍼티 어트리뷰트
- 내부 슬롯과 내부 메서드
- ECMAScript 사양부터 등장한 이중 대괄호 ( [[...]] )로 감싼 이름들을 지칭
- 개발자가 직접 접근할 수 있도록 외부로 공개된 객체의 프로퍼티는 아니다 ( 자바스크립트 엔진의 내부 로직이다 )
- 일부 내부 슬롯과 내부 메서드에 한하여 간접적으로 접근은 가능하다
- 프로퍼티 어트리뷰트와 프로퍼티 디스크립터 객체
- 자바 스크립트 엔진은 프로퍼티를 생성할 때 프로퍼티의 상태를 나타내는 프로퍼티 어트리뷰트를 기본값으로 자동 정의한다
- 프로퍼티 상태
- 프로퍼티의 값
- 값의 갱신 가능 여부
- 열거 가능 여부
- 재정의 가능 여부
- 프로퍼티 디스크립터 객체 받아오기
- Object.getOwnPropertyDescriptor(접근할 객체, 프로퍼티 "key" 값 )
- Object.getOwnPropertyDescriptors(접근할 객체, 프로퍼티 "key" 값 )
- 데이터 프로퍼티와 접근자 프로퍼티
- 데이터 프로퍼티
- 키와 값으로 구성된 일반적인 프로퍼티
- [[value]] : 프로퍼티 값
- [[Writable]] : 프로퍼티 값의 변경 가능 여부, boolean
- [[Enumerable]] : 프로퍼티의 열거 가능 여부, boolean
- [[Configurable]] : 프로퍼티의 재정의 가능 여부, boolean
- 접근자 프로퍼티
- 자체적으로 값을 갖지 않고, 다른 데이터 프로퍼티의 값을 읽거나 저장할 때 호출되는 접근자 함수로 구성
- [[Get]] : 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 읽을 때 호출되는 접근자 함수다
- [[Set]] : 접근자 프로퍼티를 통해 데이터 프로퍼티의 값을 저장할 때 호출되는 접근자 함수다
- [[Enumerable]] : 데이터 프로퍼티의 Enumerable과 같다
- [[Configurable]] : 데이터 프로퍼티의 Configurable과 같다
- 프로토 타입(prototype)
- 어떤 객체의 상위(부모) 객체 역할을 하는 객체이다.... ( 그냥 부모역할 객체... 말을 너무 어렵게 써놨.. )
- 하위 객체에게 자신의 프로퍼티와 메서드를 상속한다
- 상속 받은 하위 객체는 자신의 프로퍼티 또는 메서드인 것처럼 자유롭게 사용 할 수 있다
- 데이터 프로퍼티
- 프로퍼티 정의
- 새로운 프로퍼티를 추가하면서 프로퍼티 어트리뷰트를 명시적으로 정의하거나, 기존 프로퍼티의 프로퍼티 어트리뷰트를 재정의 하는 것을 말한다
- Object.defineProperty: 한번에 하나의 프로퍼티만 정의
- Object.defineProperties: 여러 개의 프로퍼티를 한번에 정의
- 객체 변경 방지
- 객체는 변경 가능한 값이므로, 재할당 없이 직접 변경할 수 있다
- 프로퍼티를 추가, 삭제, 갱신등을 직접 할 수 있으며, 프로퍼티 정의 메서드를 통해 프로퍼티 어트리뷰트를 재정의할 수도 있다
- 객체 변경 방지 메서드
구분 메서드 프로퍼티 추가 프로퍼티 삭제 프로퍼티 값 읽기 프로퍼티 값 쓰기 프로퍼티 어트리뷰트 재정의 객체 확장 금지 Object.preventExtensions X O O O O 객체 밀봉 Object.seal X X O O X 객체 동결 Object.freeza X X O X X - 객체 확장 금지
- Object.preventExtensions
- 확장이 금지된 객체는 프로퍼티 추가가 금지된다
- Object.isExtensible 메서드로 확장이 가능한 객체인지 확인할 수 있다
- 객체 밀봉
- Object.seal
- 프로퍼티 추가 및 삭제와 프로퍼티 어트리뷰트 재정의 금지를 의미
- 밀봉된 객체는 읽기와 쓰기만 가능하다
- Object.isSealed 메서드로 확인 가능하다
- 객체 동결
- Object.freeze
- 읽기만 가능하다
- Object.isFrozen 메서드로 확인 가능하다
- 불변 객체 만들기
- 지금까지 살펴본 변경 방지 메서드들은 얕은 변경 방지(shallow only)로 직속 프로퍼티만 변경이 방지되고 중첩 객체까지는 영향을 주지 못한다
- 객체의 중첩 객체까지 동결하여 변경이 불가능한 읽기 전용의 불변 객체를 구현하려면 객체를 값으로 갖는 모든 프로퍼티에 대해 재귀적으로 Object.freeze 메서드를 호출해야한다
// 16-1
function deepFreeze(target) {
if (target && typeof target === 'object' && !Object.isFrozen(target)) {
Object.freeze(target);
Object.keys(target).forEach(key => deepFreeze(target[key]));
}
return target;
}
const person = {
name: 'Lee',
address: { city: 'Seoul' }
};
deepFreeze(person);
console.log(Object.isFrozen(person)); // true
console.log(Object.isFrozen(person.address)); // true
person.address.city = 'Busan';
console.log(person); // { name: "Lee", address: {city: "seoul"}}
17장. 생성자 함수에 의한 객체 생성
- 객체 리터럴에 의한 객체 생성 방식은 가장 일반적이고 간단한 객체 생성 방식이다
- new 연산자와 함께 Object 생성자 함수를 호출하면 빈 객체를 생성하여 반환한다 ( 프로퍼티, 메서드등을 추가로 넣어 객체를 완성 )
- Object 생성자 함수 이외에도 String, Number, Boolean, Function, Array, Date, Promise 등의 빌트인 생성자 함수를 지원
- 객체 리터럴에 의한 객체 생성 방식의 문제점
- 객체 리터럴에 의한 객체 생성 방식은 직관적이고 간편하다
- 하지만 매번 비슷하거나 동일한 프로퍼티를 갖는 객체를 여러 개 생성해야 하는 경우 같은 프로퍼티를 기술해야한다
- 즉 불필요한 코딩 양이 늘어난다
// 17-1 const circle1 = { radius: 5, getDiameter() { return 2 * this.radius; } }; console.log(circle1.getDiameter()); // 10 const circle2 = { radius: 10, getDiameter() { return 2 * this.radius; } } console.log(circle2.getDiameter()); // 20
- 생성자 함수에 의한 객체 생성 방식의 장점
- 템플릿(클래스)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성할 수 있다
// 17-2 function Circle(radius) { this.radius = radius; this.getDiameter = function () { return 2 * this.radius; }; } const circle1 = new Circle(5); const circle2 = new Circle(10); console.log(circle1.getDiameter()); // 10 console.log(circle2.getDiameter()); // 20
- 템플릿(클래스)처럼 생성자 함수를 사용하여 프로퍼티 구조가 동일한 객체 여러 개를 간편하게 생성할 수 있다
- 생성자 함수의 인스턴스 생성 과정
- 자바 스크립트 엔진은 생성자 함수(new 키워드)를 호출하는 순간 암묵적으로 빈 객체를 생성하고 반환한다
- 생성 과정
- new 키워드를 통해 생성자 함수 호출
- 인스턴스 생성과 this 바인딩( 바인딩이란 식별자와 값을 연결하는 과정을 의미 )
- 인스턴스 초기화 ( 개발자가 직접 기술하는 내용 )
- 인스턴스 반환 ( 위 처리가 모두 끝나면 완성된 인스턴스가 바인딩된 this를 반환 )
- 내부 메서드 [[Call]] 과 [[Construct]]
- 함수는 객체이므로 일반 객체와 동일하게 동작할 수 있다
- 일반 객체는 호출이 불가능하지만 함수 객체는 호출이 가능하다
- 일반 객체가 가지고 있는 내부 슬롯과 내부 메서드를 가진다
- 함수로서 동작하기 위해 함수 객체만을 위한 [[Environment]], [[FormalParameters]]
- 내부 슬롯과 [[Call]], [[Construct]] 같은 내부 메서드를 추가로 가지고 있다
// 17-3 function foo() {} // 일반적인 함수로서 호출: [[Call]]이 호출된다 foo(); // 생성자 함수로서 호출: [[Construct]]가 호출된다 new foo();
- constructor와 non-constructor의 구분
- constructor: 함수 선언문, 함수 표현식, 클래스(클래스도 함수다)
- non-constructor: 메서드(ES6 메서드 축약 표현), 화살표 함수
// 17-3 function foo() {} const bar = function () {}; const baz = { x: function() {} }; new foo(); // -> foo {} new bar(); // -> bar {} new baz.x(); // -> x {} const arrow = () => {}; new arrow(); // TypeError: arrow is not a constructor const obj = { x() {} }; new obj.x(); // TypeError: obj.x is not a constructor
- 함수가 어디에 할당되어 있는지에 따라 메서드인지를 판단하는 것이 아니라, 함수 정의 방식에 따라 constructor와 non-constructor를 구분한다
- 즉 함수 선언문과 함수표현식으로 정의된 함수만이 constructor이다
- new 연산자
- new 연산자와 함께 함수를 호출하면 해당 함수는 생성자 함수로 동작한다
- 함수 객체의 내부 메서드 [[Call]]이 호출되는 것이 아니라, [[Construct]]가 호출된다
- 단, new 연산자와 함께 호출되는 함수는 constructor 이어야한다
- new.target
- 생성자 함수로서 역할을 더 명확히 하기위해 쓴다
- new 연산자 없이 호출되는 것을 방지하고, 강제로 new를 호출할 수도 있다
// 17-4 function Circle(radius) { if (!new.target) { return new Circle(radius); } this,radius = radius; this.getDiameter = function () { return 2 * this.radius; }; } const circle = Circle(5); // new 키워드 없이도 생성자 함수 호출
- new.target은 ES6에서 도입된 최신 문법이라 IE에서는 지원하지 않는다.
- 스코프 세이프 생성자 패턴을 이용하면 된다
// 17-5 function Circle(radius) { if ( !(this instanceof Circle)) { return new Circle(radius); } this,radius = radius; this.getDiameter = function () { return 2 * this.radius; }; } const circle = Circle(5); // new 키워드 없이도 생성자 함수 호출
18장. 함수와 일급 객체
- 일급 객체
- 조건
- 무명의 리터럴로 생성할 수 있다. 즉 런타임에 생성이 가능하다
- 변수나 자료구조(객체,배열 등)에 저장할 수 있다
- 함수의 매개변수에 전달할 수 있다
- 함수의 반환값으로 사용할 수 있다
- 가장 큰 특징은 일반 객체와 같이 함수의 매개변수에 전달할 수 있으며, 함수의 반환값으로 사용할 수도 있다는 것
- 함수형 프로그래밍을 가능케하는 초석이 된다
- 조건
- 함수 객체의 프로퍼티
- arguments 프로퍼티
- 함수 호출 시 전달된 인수들의 정보를 담고 있는 순회 가능한 유사 배열 객체
- 함수 내부에서 지역 변수처럼 사용되며, 외부에서는 참조 불가능
- 자바 스크립트는 함수의 매개변수와 인수의 갯수가 일치하는지 확인하지 않는다. ( 함수 호출시 매개변수 개수 만큼 인수를 전달하지 않아도 에러가 발생하지 않음 )
- arguments 객체는 매개변수 개수를 확정할 수 없는 가변 인자 함수를 구현할 때 용이하다
- caller 프로퍼티
- 함수 자신을 호출한 함수를 가르킴
- length 프로퍼티
- 함수를 정의할 때 선언한 매개변수의 개수를 가르킨다
- name 프로퍼티
- 함수 이름을 나타냄
- __proto__ 접근자 프로퍼티
- 간접적으로 프로토타입 객체에 접근할 수 있게 해준다
- prototype 프로퍼티
- 생성자 함수로 호출할 수 있는 함수 객체, 즉 constructor만이 소유한 프로퍼티이다
- 생성자 함수가 생성할 인스턴스의 프로토타입 객체를 가르킨다
- arguments 프로퍼티
728x90
반응형