티스토리 뷰
가. JavaScript의 객체지향 특징
클래스는 없고 프로토타입만 있다
- '인스턴스화 및 인스턴스'라는 개념은 존재하나 클래스가 없고, '프로토타입(모형)' 개념만 존재
가장 간단한 클래스 정의하기
var Member = function() {}; //자바스크립트의 Member 클래스 var mem = new Member(); // new 연산자로 인스턴스화
JavaScript에서는 함수(
Function
객체)에 클래스의 역할을 부여한다.애로우 함수에서는 생성자를 정의할 수 없다(ES2015)
let Member = () => { ... 생성자의 내용 ...}; let m = new Member(); // Error : Member is not a constructor
(ES2015) 환경에서는 순수하게 class 명령을 이용해야 한다.
생성자로 초기화하기
var Member = function(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; this.getName = function() { return this.lastName + ' '+ this.firstName; } }; var mem = new Member('철수', '강'); console.log(mem.getName()); //결과 : 강 철수
this
키워드는 생성자에 의해 생성되는 인스턴스를 나타냄- 함수 객체로서의 값이 프로퍼티의 메소드로 간주됨.
동적으로 메소드 추가하기
메소드를 나중에 정의하는 방식
var Member = function(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } var mem = new Member('철수', '강'); mem.getName = function() { return this.lastName + ' ' + this.firstName; }//생성된 인스턴스에 대해 메소드가 추가되고 있음. console.log(mem.getName());// 결과 : 강 철수
인스턴스에 대해서 직접 멤버(프로퍼티 or 메소드)를 추가하는 방식
var Member = function(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } var mem = new Member('철수', '강'); mem.getName = function() { return this.lastName + ' ' + this.firstName; } console.log(mem.getName()); //결과 : 강 철수 var mem2 = new Member('영희', '이'); console.log(mem2.getName()); // error : mem2.getName is not a function
문맥에 따라 내용이 변하는 변수 - this 키워드
this 키워드가 참조하는 곳
톱 레벨(함수의 바깥) - 글로벌 객체
함수 - 글로벌 객체(
strict
모드에서는undefined
)call
/apply
메소드 - 인수로 지정된 객체func.call(that [,arg1 [,arg2 [,...]]]) func.apply(that [,args]) /* func : 함수 객체 that : 함수 안에서 this 키워드가 가리키는 것 참고로 인수 that에 null을 건넬경우, 암묵적으로 글로벌 객체가 건네진 것으로 간주. arg1, arg2, ... : 함수에 건넬 인수 args : 함수에 건넬 인수(배열) */
//call method 예시. apply 메소드의 경우도 동일함. var data = 'Global data'; var obj1 = { data: 'obj1 data' }; var obj2 = { data: 'obj2 data' }; function hoge() { console.log(this.data); } hoge.call(null); //결과 : Global data hoge.call(obj1); //결과 : obj1 data hoge.call(obj2); //결과 : obj2 data
//배열과 비슷하지만 배열이 아닌 객체를 배열로 변환하기 function hoge() { //arguments 객체를 this로 해서 Array.slice 객체를 호출하시오. var args = Array.prototype.slice.call(arguments); console.log(args.join('/')); } hoge('Angular', 'React', 'Backbone'); // 결과 : Angular/React/Backbone
이벤트 리스너 - 이벤트의 발생처
생성자 - 생성한 인스턴스
메소드 - 호출원의 객체(= 리시버 객체)
생성자의 강제적인 호출
var Member = function(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }; var m = Member('인식', '정'); console.log(m); //결과 : undefined console.log(firstName); //결과 : 인식 console.log(m.firstName); // 결과 : 에러(Cannot read property 'firstName' of undefined)
이 경우
Member
객체는 생성되지 않고, 대신 글로벌 변수로firstName
/lastName
이 생성되어 버린다(this
가 글로벌 객체를 나타내고 있기 때문)따라서
var Member = function(firstName, lastName) { if(!(this instanceof Member)) {//여기서 this는 Member 객체가 아니라 글로벌 객체. return new Member(firstName, lastName);//따라서 아닌 경우 new 연산자로 호출. } this.firstName = firstName; ...중략... };
나. 생성자의 문제점과 프로토타입
생성자에 의한 메소드 추가 → 메소드의 수에 비례하여 '쓸데없이' 메모리를 소비한다.
메모리는 프로토타입으로 선언한다 -
prototype
프로퍼티객체를 인스턴스화 했을 경우, 인스턴스는 베이스가 되는 객체에 속하는
prototype
객체에 대해서 암묵적인 참조를 갖게 된다.var Member = function(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } Member.prototype.getName = function() { return this.lastName + ' ' + this.firstName; var mem = new Member('인식' + '정'); console.log(mem.getName()); // 결과 : 정 인식 // prototype 객체에 추가된 getName 메소드가 Member 클래스의 인스턴스(변수 mem)에서도 올바르게 참조됨.
프로토타입 객체를 사용한 메소드 정의의 두가지 이점
메모리의 사용량을 절감할 수 있다.
JavaScript에서는 객체의 멤버가 호출되었을 때 다음의 흐름으로 멤버를 취득
인스턴스 측에 요구된 멤버가 존재하지 않는지 확인
존재하지 않는 경우에는 암묵적인 참조를 통해 프로토타입 객체를 검색
-> 생성자 경유로 메소드 정의시 발생하는 메모리 낭비 문제 회피.
멤버의 추가나 변경을 인스턴스가 실시간으로 인식할 수 있다.
인스턴스에 멤버를 복사하지 않는다 라고 하는 것은 프로토타입 객체로의 변경(추가나 삭제)을 인스턴스 측에서 동적으로 인식할 수 있다 라는 뜻도 된다.
var Member = function(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; } var mem = new Member('인식', '정'); //인스턴스화 후에 메소드를 추가 Member.prototype.getName = function() { return this.lastName + ' ' + this.firstName; }; console.log(mem.getName());//결과 : 정 인식 //인스턴스 생성 후에 getName 메소드 추가 -> 아무 문제 없이 메소드를 인식함.
프로토타입 객체의 불가사의(1) - 프로퍼티의 설정
var Member = function() { }; Member.prototype.sex = '남자'; var mem1 = new Member(); var mem2 = new Member(); console.log(mem1.sex + '|' + mem2.sex);// 결과 : 남자|남자 mem2.sex = '여자'; console.log(mem1.sex + '|' + mem2.sex);// 결과 : 남자|여자
- 프로토타입 객체가 사용되는 것은 '값을 참조할 때 뿐'이다. 값의 설정은 항상 인스턴스에 대해 행해진다.
프로토타입 객체의 불가사의(2) - 프로퍼티의 삭제
var Member = function() { }; Member.prototype.sex = '남자'; var mem1 = new Member(); var mem2 = new Member(); console.log(mem1.sex + '|' + mem2.sex);// 결과 : 남자|남자 mem2.sex = '여자'; console.log(mem1.sex + '|' + mem2.sex);// 결과 : 남자|여자 delete mem1.sex delete mem2.sex console.log(mem1.sex + '|' + mem2.sex); // 결과 : 남자 | 남자
인스턴스 측에서 멤버의 추가나 삭제 조작이 프로토타입 객체에까지 영향을 미칠 일은 없다.
인스턴스 단위로 프로토타입 멤버 삭제하기
delete Member.prototype.sex
- 참조하고 있는 모든 인스턴스에 영향을 미친다.
프로토타입에서 정의된 멤버를 인스턴스 단위로 삭제하고 싶은 경우 ->
undefined
를 이용var Member = function() {}; Member.prototype.sex = '남자'; var mem1 = new Member(); var mem2 = new Member(); console.log(mem1.sex + '|' + mem2.sex); // 결과 : 남자|남자 mem2.sex = undefined; console.log(mem1.sex + '|' + mem2.sex); // 결과 : 남자|undefined // 덮어씀으로써 '유사적으로' 멤버를 무효화
객체 리터럴로 프로토타입 정의하기
지금까지의 방법
var Member = function(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; }; Member.prototype.getName = function(){ return this.lastName + ' ' + this.firstName; }; Member.prototype.toString = function(){ return this.lastName + this.firstName; };
- 귀찮고, 가독성이 좋지 않음
객체 리터럴을 이용하는 방법
var Member = function(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; }; Member.prototype = { getName : function(){ return this.lastName + ' ' + this.firstName; }, toString : function(){ return this.lastName + this.firstName; } };
- 얻을 수 있는 효과
Member.prototype.~
와 같은 기술이 최소한으로 억제된다.- 그 결과, 객체명의 변경이 있을 경우에도 영향을 받는 범위는 한정 가능하다.
- 동일 객체의 멤버 정의가 하나의 블록에 담겨져 있기 때문에 코드의 가독성이 향상된다.
- 얻을 수 있는 효과
정적 프로퍼티 / 정적 메소드 정의하기
정적 프로퍼티 / 정적 메소드란 인스턴스를 생성하지 않아도 객체로부터 직접 호출할 수 있는 프로퍼티 / 메소드이다.
//정적 프로퍼티/정적 메소드는 다음과 같이 생성자에 직접 추가한다 객체명.프로퍼티명 = 값 객체명.메소드명 = function() { /*메소드의 정의*/ }
정적 프로퍼티 / 정적 메소드를 정의할 때 두가지 주의점
- 정적 프로퍼티는 기본적으로 읽기 전용의 용도로 사용
- 정적 메소드 안에서는
this
키워드를 사용할 수 없다.
왜 정적 멤버를 정의하는 것인가?
- 글로벌 변수 / 함수는 이름이 충돌하는 원인이 된다. 그러기 때문에 가능한 적게 사용해야 한다. 이를 위해 정적 멤버를 정의함으로서 사용을 적게 하는 역할을 하게 된다.
다. 객체의 계승 - 프로토타입 체인
프로토타입 체인의 기초
var Animal = function() {}; Animal.prototype = { walk : function() { console.log('종종...'); } }; var Dog = function() { Animal.call(this); }; Dog.prototype = new Animal();//Dog 객체의 프로토타입이 Animal 객체의 인스턴스로 세트 Dog.prototype.bark = function(){ console.log('멍멍!! '); } var d = newDog(); d.walk(); d.bark();
- 호출의 흐름
Dog
객체의 인스턴스 d로부터 멤버의 유무를 검색한다- 해당하는 멤버가 존재하지 않는 경우에는
Dog
객체의 프로토타입 - 즉Animal
객체의 인스턴스를 검색한다 - 거기서도 목적의 멤버가 발견되지 않은 경우에는 한층 위의
Animal
객체의 프로토타입을 검색한다.(예제) - (만약 여기서도 발견되지 않은 경우) 한층 더 위의 프로토타입(구체적으로는 최상위의
Object.prototype
까지)으로 거슬러 올라가게 된다.
- 호출의 흐름
계승 관계는 동적으로 변경 가능
var Animal = function() {}; Animal.prototype = { walk : function() { console.log('종종...'); } }; var SuperAnimal = function() {}; SuperAnimal.prototype = { walk : function() { console.log('다다다닷!'); } }; var Dog = function() {}; Dog.prototype = new Animal(); var d1 = newDog(); d1.walk(); // 결과 : 종종... Dog.prototype = new SuperAnimal(); //SuperAnimal 객체를 계승 var d2 = new Dog(); d2.walk(); //결과 : 다다다닷! d1.walk(); //결과 : ????? -> 결과는 종종...
- Javascript의 프로토타입 체인은 인스턴스가 생성된 시점에서 고정되어 그 후의 변경에는 관여치 않고 보존된다.
객체의 타입 판정하기
스크립트 상에서 다루고 있는 타입을 판정하는 방법
바탕이 되는 생성자 취득하기 -
constructor
프로퍼티//Animal 클래스와 이것을 계승한 Hamster 클래스 준비 var Animal = function() {}; var Hamster = function() {}; Hamster.prototype = new Animal(); var a = new Animal(); var h = new Hamster(); console.log(a.constructor === Animal); // 결과 : true console.log(h.constructor === Animal); // 결과 : true console.log(h.constructor === Hamster); // 결과 : false
- 프로토타입 계승을 하는 경우에는
constructor
프로퍼티가 나타내는 것이 상속원의 클래스(예제에서는Animal
클래스)가 된다.
- 프로토타입 계승을 하는 경우에는
바탕이 되는 생성자 판정하기 -
instanceof
연산자객체가 특정 생성자에 의해 생성된 인스턴스인지의 여부를 판정
console.log(h instanceof Animal); //결과 : true console.log(h instanceof Hamster); //결과 : true
instanceof
연산자는 본래의 생성자(Hamster
)는 물론이고 프로토타입 체인을 거슬러 올라가는 판정도 가능하다.(이 예에서는Animal
)
참조하고 있는 프로토타입 확인하기 -
isPrototypeOf
메소드객체가 참조하는 프로토타입을 확인하는데 이용한다
console.log(Hamster.prototype.isPrototypeOf(h)); //결과 : true console.log(Animal.prototype.isPrototypeOf(h)); //결과 : true
멤버의 유무 판정하기 -
in
연산자var obj = { hoge: function(){}, foo: function(){} }; console.log('hoge' in obj); // 결과 : true console.log('piyo' in obj); // 결과 : false
라. 본격적인 개발에 대비하기
private
멤버 정의하기JavaScript에서는
private
멤버를 정의하기 위한 구문은 없으나, 클로저를 이용해 유사하게 구현 가능.function Triangle(){ //(1)scope start //private 프로퍼티의 정의(밑변/높이를 보존) var _base; var _height; // private 메소드의 정의(인수가 올바른 숫자인가를 체크) var _checkArgs = function(val){ return (typeof val === 'number' && val > 0); } //(1)scope end //(2)scope start //private 멤버에 엑세스하기 위한 메소드의 정의 this.setBase = function(base){ if(_checkArgs(base)){ _base = base;} } this.getBase = function() {return _base;} this.setHeight = function(height){ if(_checkArgs(height)){ _height = height; } } this.getHeight = function() { return _height; } //(2)scope end } //private 멤버에 액세스하지 않는 보통의 메소드를 정의 Triangle.prototype.getArea = function() { return this.getBase() * this.getHeight() / 2; } var t = new Triangle(); t._base = 10; t._height = 2; console.log('삼각형의 면적 : ' + t.getArea()); // 결과 : NaN t.setBase(10); t.setHeight(2); console.log('삼각형의 밑변 : ' + t.getBase()); //결과 : 삼각형의 밑변 : 10 console.log('삼각형의 높이 : ' + t.getHeight()); //결과 : 삼각형의 높이 : 2 console.log('삼각형의 면적 : ' + t.getArea()); // 결과 : 삼각형의 면적 : 10
_base
/_height
프로퍼티,_checkArgs
메소드가private
멤버이다.private
멤버는 생성자 함수에서 정의한다.//private 프로퍼티/메소드 var 프로퍼티명 var 메소드명 = function(인수, ...) { ... 임의의 처리 ...}
private
멤버를 정의하는 경우this
키워드가 아니라var
키워드로 선언한다.
'
privilege
메소드'를 정의해private
멤버에 엑세스하기private
멤버에 엑세스할 수 있는 메소드를privileged
메소드라고 한다. '생성자 함수 안에서 정의한다'는 점만 빼면 일반적인 메소드와 같은 방법으로 정의 가능함.
엑세서 메소드 경유로 프로퍼티 공개하기
- 프로퍼티 그 자체는 클래스 외부로부터 엑세스 할 수 없게 해두고, 대신에 프로퍼티에 엑세스하기 위한 메소드 ( = 엑세서 메소드 (참조용 : 게터 메소드, 설정용 : 세터 메소드))
- 사용하는 경우
- 값을 읽기(쓰기) 전용으로 하고 싶다.
- 값을 참조할 때 데이터를 가공하고 싶다.
- 값을 설정할 때 타당성을 검증하고 싶다.
- 일반적으로 'get + 프로퍼티명', 'set + 프로퍼티명'의 형식으로 명명함. 프로퍼티명의 머리글자는 대문자로 한다.
Object.defineProperty
메소드에 의한 엑세서 메소드 구현//defineProperty 메소드 Object.defineProperty(obj, prop, desc) obj : 프로퍼티를 정의하는 객체 prop : 프로퍼티명 desc : 프로퍼티의 구성 정보
defineProperty
메소드는 인수desc
에 대해서get
/set
파라미터를 지정함으로서 게터 / 세터를 정의할 수 있음.
function Triangle() { // private 변수를 선언 var _base; var _height; // base 프로퍼티를 정의 Object.defineProperty( this, 'base', { get: function() { return _base; } set: function(base) { if(typeof base === 'number' && base > 0) { _base = base; } } } );
//height 프로퍼티를 정의
Object.defineProperty(
this,
'height',
{
get: function(){
return _height;
}
set: function(){
if(typeof height === 'number' && height > 0 ) {
_height = height;
}
}
}
);
};
Triangle.prototype.getArea = function() {
return this.base * this.height / 2 ;
};
var t = new Triangle();
t.base = 10;
t.height = 2;
console.log('삼각형의 밑변 : ' + t.base); // 결과 : 삼각형의 밑변 : 10
console.log('삼각형의 높이 : ' + t.height); // 결과 : 삼각형의 높이 : 2
console.log('삼각형의 면적 ; ' + t.getArea); // 결과 : 삼각형의 면적 : 10
```javascript
//get/set 파라미터
get: function() {
return private변수
},
set: function(value){
private변수 : value
}
여러 프로퍼티를 함께 정의하기
//defineProperties 메소드 Object.defineProperties(obj, props) obj : 프로퍼티를 정의하는 객체 props : 프로퍼티의 구성 정보("프로퍼티명: 구성 정보"의 해시 형식)
// 위의 예시를 defineProperties 메소드 이용하여 고쳐 쓴 예제 Object.defineProperties(this, { base:{ get: function() { return _base; }, set: function(base){ if(typeof base === 'number' && base > 0){ _base = base; } } }, height: { get:function() { return _height; }, set: function(height){ if(typeof height === 'number' && height > 0){ _height = height; } } } });
네임스페이스 / 패키지 작성하기
JavaScript에는
namespace
가 없음 -> 빈 객체를 이용 유사 네임스페이스를 만들어 활용함var Wings = Wings || {};//Wings가 미정의인 경우에만 새롭게 네임스페이스를 작성한다. Wings.Member = function(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; };//Wings 네임스페이스 하에 속하는 Member 클래스 정의됨. Wings.Member.prototype = { getName : function() { return this.lastName + ' ' + this.firstName; } }; var mem = new Wings.Member('인식', '정'); console.log(mem.getName());
계층을 지닌 네임스페이스 정의하기
위의 예시를 반복함으로서 정의해도 되지만, 네임스페이스를 위한 함수를 작성하면 편의성 증가
function namespace(ns){ // 네임스페이스를 '.' 구분으로 분할 var names = ns.split('.'); var parent = window; //namespace를 상위부터 순서대로 등록 for(var i = 0, len = names.length; i<len; i++){ parent[names[i]] = parent[names[i]] || {}; parent = parent[names[i]]; } return parent; } //Wings.Gihyo.Js.MyApp 네임스페이스를 등록 var my = namespace('Wings.Gihyo.Js.MyApp'); my.Person = function() {}; var p = new my.Person(); console.log(p instanceof Wings.Gihyo.Js.MyApp.Person); // 결과 : true
Wings.Gihyo.Js.MyApp.Person
을my.Person
(별명)으로 표현할 수 있게 됨.
마. ES2015의 객체지향 구문
클래스 정의하기 -
class
명령(ES2015)firstName
/lastName
프로퍼티,getName
메소드를 가지는Member
클래스를class
명령을 이용해 정의하기class Member { //생성자 constructor(firstName, lastName){ this.firstName = firstName; this.lastName = lastName; } //메소드 getName() { return this.lastName + this.firstName; } } let m = new Member('시온', '정'); console.log(m.getName()); //결과 : 정시온
/*
class 명령
class 클래스명 {
...생성자의 정의...(constructor로 고정)
...프로퍼티의 정의...
...메소드의 정의...
}
생성자/메소드의 정의
메소드명(인수, ...) {
... 메소드의 본체 ...
}
*/
```
클래스는
function
생성자와 다르다.함수로서 호출할 수 없다.
let m = Member('시온', '정');//new 연산자가 없다 //결과 : Class constructor Member cannot be invoked without 'new'
정의하기 전의 클래스를 호출할 수 없다.
let m = new Member('시온', '정'); // 결과 : Member is not defined class Member{ ...생략...}
프로퍼티 정의하기
class Member { //생성자 constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } //firstName 프로퍼티 get firstName() { return this._firstName; } set firstName(value) { this._firstName = value; } //lastName 프로퍼티 get lastName() { return this._lastName; } set lastName(value) { this._lastName = value; } getName() { return this.lastName + this.firstName; } } let m = new Member('시온', '정'); console.log(m.getName());//결과 : 정시온
정적 메소드 정의하기(
static
)class Area { static getTriangle(base, height) { return base * height / 2; } } console.log(Area.getTriangle(10, 5));// 결과 : 25
//from MDN class Triple { static triple(n) { // 이전에 default 지원 안될 때 n = n || 1; //비트연산이 아니어야 합니다. return n * 3; } } class BiggerTriple extends Triple { static triple(n) { return super.triple(n) * super.triple(n); } } console.log(Triple.triple()); // 3 console.log(Triple.triple(6)); // 18 console.log(BiggerTriple.triple(3)); // 81 var tp = new Triple(); console.log(BiggerTriple.triple(3)); // 81 (부모의 인스턴스에 영향을 받지 않습니다.) console.log(tp.triple()); // 'tp.triple은 함수가 아닙니다.'. console.log(tp.constructor.triple(4)); // 12
기존 클래스 계승하기
class Member { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } getName() { return this.lastName + this.firstName; } } //Member 객체를 계승한 BusinessMember 클래스를 정의 class BusinessMember extends Member { work() { return this.getName() + '은 공부하고 있습니다.'; } } let bm = new BusinessMember('시온', '정'); console.log(bm.getName()); console.log(bm.work());
기본 클래스의 메소드 / 생성자 호출하기 -
super
키워드기본 클래스에 정의된 메소드 / 생성자를 서브 클래스에서 재정의 -> 메소드의 오버라이드
//Member 클래스에 정의된 생성자/getName 메소드를 BusinessMember 클래스에서 오버라이드 하는 예시 class Member { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } getName() { return this.lastName + this.firstName; } } //Member 객체를 계승한 BusinessMember 클래스를 정의 class BusinessMember extends Member { // 인수로 clazz를 추가 constructor(firstName, lastName, clazz) { super(firstName, lastName); this.clazz = clazz; } // 직책 포함의 이름을 반환하도록 수정 getName() { return super.getName() + '/직책:' + this.clazz; } } let bm = new BusinessMember('성룡', '김', '과장'); console.log(bm.getName());// 결과 : 김성룡 / 직책 : 과장
- 오버라이드시, 기본 클래스의 기능을 완전히 재작성 하는 것 만은 아님. 이어 받으며 나머지만 추가하는 경우도 있음. 이럴 경우
super
키워드로 참조할 수 있음.
- 오버라이드시, 기본 클래스의 기능을 완전히 재작성 하는 것 만은 아님. 이어 받으며 나머지만 추가하는 경우도 있음. 이럴 경우
객체 리터럴의 개선(ES2015)
- 메소드 정의하기
- 변수를 동일 명칭의 프로퍼티에 할당하기
애플리케이션을 기능 단위로 모으기 - 모듈(ES2015)
- 모듈의 기본
import
명령의 다양한 표기법- 모듈 전체를 한꺼번에 가져오기
- 모듈 안의 개별 요소에 별명 부여하기
- 디폴트의
export
가져오기
- 브라우저 환경에서 모듈 동작하기
- 보충설명 :
private
멤버 정의하기 -symbol
열거 가능한 객체 정의하기 - 반복자(ES2015)
열거 가능한 객체를 더욱 간단하게 구현하기 - 발생자(ES2015)
객체의 기본적인 동작을 사용자 정의하기 -
Proxy
객체(ES2015)
'프로그래밍 > JavaScript' 카테고리의 다른 글
함수 (0) | 2020.02.11 |
---|---|
기본 데이터 조작하기 (0) | 2020.02.11 |
기본적인 작성법 익히기 (0) | 2020.02.06 |