ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TypeScript] 핸드북 정리(7) - Class
    TypeScript 2023. 4. 19. 23:52

    Class

    타입스크립트는 자바스크립트의 여러가지 ES6 이후의 문법들을 지원합니다.

    클래스 역시 타입스크립트에서 사용이가능합니다.

     

    class Person {}

    기본적인 클래스 문법입니다. 현재는 빈 클래스입니다.

     

     

    class Person {
    	name:string;
    	age:number;
    }
    
    const choi = new Person();
    choi.name = 'choi'; // 'choi':string;
    choi.name = 29; // error number는 string타입에 할당 할 수 없음
    choi.age = 29; // 29:number;

     

    클래스는 new 연산자와 함께 호출하여 인스턴스를 생성하고, 기본적인 필드를 정의할 수 있습니다.

    필드의 타입은 필수는 아니지만, 따로 타입을 정의하지 않으면 any 타입을 갖습니다.

    필드는 생성된 인스턴스에서 사용이 가능합니다.

     

    class Person {
    	name:string;
    	age:number;
      readonly gender:string = 'male';
    }
    
    const choi = new Person();
    choi.name = 'choi'; // 'choi':string;
    choi.age = 29; // 29:number;
    choi.gender = 'female'; // 읽기 전용속성이므로 gender에 할당불가능

     

    필드에 readonly 속성을 사용하면 다른 타입과 마찬가지로 재할당이 불가능합니다.

     

     

    constructor

    class Person {
    	name: string;
    	age: number;
            constructor(name = '', age = 0) {
                this.name = name;
                this.age = age;
            }
    }

     

     

    위의 예제처럼 기본적인 필드를 정의하는 constructor를 정의할 수도 있고,

     

    class Person {
    	name: string;
    	age: number;
        	constructor(name: string; age: number);
        	constructor(job: string);
            constructor(name? = '', age? = 0) {
                this.name = name;
                this.age = age;
            }
    }

     

    constructor를 오버로딩할 수도 있고, 필드를 옵셔널로 정의할 수도 있습니다.

    다만, constructor는 제네릭을 사용할 수 없습니다.

     

     

    super

    class Person {
    	name: string;
    	age: number;
      constructor(name: string, age: number) {
          this.name = name;
          this.age = age;
      }
    }
    
    class Choi extends Person {
      job: string;
      constructor(job: string;) {
        this.job = job; // error super가 먼저 호출되어야함
        super()
      }
    }

     

    자바스크립트의 클래스에서도 상속받은 클래스의 constructor 안에서 super호출을 가장 먼저 해야 하듯 타입스크립트도 마찬가지입니다.

     

    Method

    let x:number = 1;
    
    class X {
      x: string = 'world';
    
      editX() {
        x = 'hello'; // error string은 number에 할당 할 수 없습니다.
      }
    }

     

    메서드는 클래스나, 객체 내부에서 화살표 함수로 정의된 함수를 뜻하는데요, 메서드에서 해당 객체 혹은 클래스의 프로퍼티에 접근할 때는 this를 사용해야 합니다.

    위의 editX 메서드는 전역변수 x 를 참조하여 에러가 발생했습니다.

     

     

    getter/setter

    class X {
      x: string = 'world';
    
      get getX(): string {
        return this.x;
      }
      
      set setX(str: string) {
        this.x = str;
      }
    }
    
    const y = new X();
    
    y.setX = 1; // error
    y.setX = 'hello';
    
    console.log(y.getX);
    console.log(y.getX()); // error

     

    getter와 setter는 프로퍼티 내부의 값을 조회하거나 갱신하는 메서드입니다.

    정의할 때는 앞에 get, set 키워드를 사용합니다.

    getter는 함수호출 시그니처를 사용하지 않습니다 참조하듯 사용해야 합니다.

    setter도 마찬가지로 호출 시그니처를 사용하지 않고 값을 할당해 주면 됩니다.

     

     

    Index Signature

    클래스의 필드도 인덱스 시그니처로 접근이 가능합니다.

    class X {
      [s: string]: string | ((s: string) => string | void)
      x: string = 'TS Class';
    
      returnX(): string {
        return this.x;
      }
    
      setX(str: string) {
        this.x = str;
      }
    }

    메서드 혹은 필드의 타입을 인덱스 시그니처로 접근이 가능합니다.

     

     

    Class Heritage

     

    implement Clauses

    interface를 이용해서 클래스의 구현부를 정의할 수 있습니다.

    interface Pingable {
      ping(): void;
    }
    
    interface Pongable {
    	pong(): void;
    }
     
    class Sonar implements Pingable {
      ping() {
        console.log("ping!");
      }
    }
     
    class Ball implements Pingable { // error
      pong() { // error
        console.log("pong!");
      }
    }
    
     
    class Ball implements Pingable, Pongable {
      ping() {
        console.log("ping!");
      }
      pong() {
        console.log("pong!");
      }
    }

    인터페이스는 implements를 이용해 클래스에 상속해 줍니다.

    상속받은 클래스는 인터페이스에 정의된 값을 반드시 구현해야 합니다.

     

    implements에 (,)를 사용하여 두 개 이상의 인터페이스를 상속받을 수 있습니다.

     

    interface Checkable {
      check(name: string): boolean;
    }
     
    class NameChecker implements Checkable {
      check(s) { // 인터페이스에서는 name 이라는 이름이었지만 s를 사용해도 에러가 발생하지않음
        // Notice no error here
        return s.toLowercse() === "ok";
      }
    }

     

    인터페이스는 클래스를 구현하는 방법을 알려주는 설계도일 뿐 클래스를 강제하거나 어떤 영향을 미치지 않습니다.

     

     

    interface Checkable {
      check(name: string): boolean;
      age?(age: number): void;
    }
    
    class NameChecker implements Checkable {
      check(s) { // 인터페이스에서는 name 이라는 이름이었지만 s를 사용해도 에러가 발생하지않음
        // Notice no error here
        return s.toLowercse() === "ok";
      }
    }
    
    const choi = new NameChecker();
    choi.check('choi');
    choi.age(); // error

    옵셔널 한 타입 역시 강제가 아니기 때문에 정의하지 않아도 됩니다, 다만 없는 값이니 당연히 사용하려 하면 에러를 발생시킵니다.

     

     

    extends Clauses

    클래스도 extends 를 이용해 확장할 수 있습니다.

    class Animal {
      move() {
        console.log("Moving along!");
      }
    }
     
    class Dog extends Animal {
      woof(times: number) {
        for (let i = 0; i < times; i++) {
          console.log("woof!");
        }
      }
    }
     
    const d = new Dog();
    d.move();
    d.woof(3);

     

    상속받은 클래스는 부모 클래스의 프로퍼티를 사용할 수 있으며, 새로운 프로퍼티도 정의할 수 있습니다.

     

     

    오버라이딩 메서드

    class Base {
      greet() {
        console.log("Hello, world!");
      }
    }
     
    class Derived extends Base {
      greet(name?: string) {
        if (name === undefined) {
          super.greet();
        } else {
          console.log(`Hello, ${name.toUpperCase()}`);
        }
      }
    }
     
    const d = new Derived();
    d.greet(); // Hello World
    d.greet("reader"); // Hello READER
    
    const b: Base = d;
    b.greet();

    자식 클래스는 부모의 메서드를 오버라이딩할 수 있습니다.

    또 부모클래스를 타입으로 사용하여 자식클래스를 참조할 수도 있습니다.

     

    class Base {
      greet() {
        console.log("Hello, world!");
      }
    }
     
    class Derived extends Base {
      greet(name: string) { // error
        if(name === undefined) {
          super.greet()
        }else {
          console.log(`Hello, ${name.toUpperCase()}`);
        }
      }
    }

     

    부모 클래스의 메서드를 오버라이딩할때 유의사항은 부모 클래스 메서드의 구현사항을 따라 합니다.

    name 매개변수가 옵셔널일 때는 에러를 발생시키지 않지만, 필수값일 때는 에러를 발생시킵니다.

    부모 클래스 메서드의 구현사항에는 필수값이 아니기 때문입니다.

     

     

    초기화 순서

    class Base {
      name = "base";
      constructor() {
        console.log("My name is " + this.name);
      }
    }
     
    class Derived extends Base {
      name = "derived";
    }
     
    const d = new Derived();
    console.log(d.name) // My name is base, derived

     

    위의 예제를 보면 'My name is derived'를 예상할 수도 있습니다.

    하지만 base가 먼저 확인됩니다.

    이는 클래스를 초기화하는 단계 때문입니다. 클래스의 동작 순서는 이러합니다.

    1. 부모 클래스 초기화
    2. 부모 클래스 constructor 호출
    3. 자식 클래스 초기화
    4. 자식 클래스 constructor 호출

     

    위와 같은 순서로 초기화하기 때문에 부모클래스의 constructor의 값이 먼저 확인됩니다.

     

    Member Visibility

    멤버 변수를 외부에 노출시킬지 아닐지를 결정하는 접근제어자를 알아보겠습니다.

     

    public

    class Greeter {
      public greet() {
        console.log("hi!");
      }
    }
    const g = new Greeter();
    g.greet();

    public 키워드는 어디에서든 접근이 가능합니다. public을 명시하지 않으면 기본값입니다.

     

     

     

    protected

    class Greeter {
      public greet() {
        console.log("Hello, " + this.getName());
      }
      protected getName() {
        return "hi";
      }
    }
     
    class SpecialGreeter extends Greeter {
      public howdy() {
        console.log("Howdy, " + this.getName()); // protected 자식클래스 접근 가능
      }
    }
    const g = new SpecialGreeter();
    g.greet(); // public 접근 가능
    g.getName(); // protected 외부 접근 불가능

    protected 키워드는 부모클래스를 할당받은 자식클래스에서만 접근 가능한 키워드입니다.

     

    class Greeter {
      protected a = 1;
    }
     
    class SpecialGreeter extends Greeter {
      a = 2;
    }
    const g = new SpecialGreeter();
    console.log(g); // { a: 2 }
    console.log(g.a); // 2

     

    다만 자식클래스에서 부모클래스의 protected 키워드를 사용한 멤버변수를 외부에 노출시킬 수 있습니다.

    위의 예제에서는 자식클래스에서 public으로 변경한 예시입니다.

    class Base {
      protected x: number = 1;
    }
    class Derived1 extends Base {
      protected x: number = 5;
    }
    class Derived2 extends Base {
      f1(other: Derived2) {
        other.x = 10;
      }
      f2(other: Base) {
        other.x = 10; // error
      }
    }
    
    const base = new Base()
    const derived1 = new Derived1();
    const derived2 = new Derived2();
    
    console.log(derived2.f1(derived2));
    console.log(derived2.f2(base));

    위의 예제의 f1은 자식클래스에서 부모클래스의 protected 값에 접근이 가능합니다.

    f2는 부모클래스인 Base의 인스턴스를 받아오지만 부모클래스의 x 멤버변수는 protected이기 때문에 접근이 불가능합니다.

     

     

    private

    class Base {
      private x = 0;
    }
    class Derived extends Base { // error
      x = 1; 
    }
    
    const base = new Base();
    const derived = new Derived();
    base.x; // error
    derived.x; // error

    private 키워드는 어디서든 접근이 불가능한 키워드입니다.

    protected는 부모의 멤버변수를 자식클래스에서 public 하게 변경이 가능했지만 private는 불가능합니다.

     

    다만 외부에서도 private 멤버변수에 접근하는 방법이 있습니다.

    class Base {
      private x = [1,2,3,4];
    }
    
    const base = new Base();
    console.log(base['x']); // [1,2,3,4]

    대괄호 표기법을 이용하면 외부에서도 비공개 멤버변수에 접근이 가능합니다.

    private 키워드는 결국 완전한 비공개는 아닙니다.

     

     

    비공개 키워드 간접접근

    class A {
      private x = 10;
     
      public sameAs(other: A) {
        // No error
        return other.x === this.x;
      }
    }

    private 키워드는 접근이 불가능합니다 하지만, 위의 예제처럼 어떤 값과 비공개 멤버변수의 값을 비교하는 즉, 직접적으로 값을 참조하는 것은 불가능하지만 간 적 접으로 접근은 가능합니다.

     

    타입스크립트 접근제어자는 컴파일 이후 런타임에 영향을 주지 않지만, 자바스크립트의 접근제어자 (#)는 확실한 접근제어기능을 합니다.

    private보다 확실하게 데이터의 접근제어를 필요로 한 경우 클로저나 위크맵을 이용하는 편이 더 좋습니다.

     

     

    Static Members

    class Base {
      static x = [1,2,3,4];
      private static y = 1;
      protected static z = 2;
    
      read():void {
        console.log(Base.x); // [1,2,3,4]
        console.log(Base.y); // 1
        console.log(Base.z); // 2
      }
    }
    
    class Sub extends Base {
      readMember ():void {
        console.log(Sub.x); // [1,2,3,4]
        console.log(Sub.y); // error private한 값
        console.log(Sub.z); // 2
      }
    }
    const base = new Base();
    const sub = new Sub();
    base.read();
    sub.readMember();

     

    static 키워드를 사용하면 정적인 멤버를 정의할 수 있습니다.

    static 멤버는 상속도 가능하며, 접근제어자를 붙일 수 있습니다.

     

    Special Static Name

    멤버변수의 이름을 자유롭게 작성할 수 있는 것은 아닙니다.

    class Base {
      static name = [1,2,3,4]; // error Function.name 과 겹침
    }
    
    console.dir(Function.name)
    console.dir(Function.length)
    console.dir(Function.call)

    Function의 기본 프로퍼티인 name과 겹치게 되므로 name이라는 이름의 프로퍼티는 사용이 불가능합니다. 예약어이기 때문이죠.

    이 외에도 프로토타입에 존재하는 이름을 중복해 사용할 수 없습니다.

     

     

    Generic Class

    class Box<Type> {
      contents: Type;
      constructor(value: Type) {
        this.contents = value;
      }
    }
     
    const b = new Box("hello!"); // Box<string>

    클래스에도 제네릭을 사용할 수 있습니다.

    이때, 제네릭 클래스가 new 연산자와 함께 호출되면 함수의 매개변수와 같은 방식으로 타입추론됩니다.

     

    class Box<Type> {
      static defaultValue: Type; // error
    }

    다만 static 멤버는 제네릭 매개변수 타입을 받을 수 없습니다.

     

     

    this at Runtime in Class

    class MyClass {
      name = "MyClass";
      getName() {
        return this.name;
      }
    }
    const c = new MyClass();
    const obj = {
      name: "obj",
      getName: c.getName,
    };
    
    console.log(obj.getName()); // obj

    자바스크립트의 this는 호출된 위치에 따라 결정됩니다. 선언된 위치를 기억하는 렉시컬스코프를 따르지 않습니다.

    위의 예제도 MyClass의 메서드인 getName을 참조했지만 출력된 값은 obj의 name 프로퍼티를 출력합니다.

    class MyClass {
      name = "MyClass";
      getName() {
        return this.name;
      }
      getName2 = () => {
        return this.name;
      }
    }
    const c = new MyClass();
    const obj = {
      name: "obj",
      getName: c.getName,
      getName2: c.getName2
    };
     
    
    console.log(obj.getName()); // obj
    console.log(obj.getName2()); // MyClass

    MyClass의 멤버 변수 name을 갖기 위해 화살표 함수(arrow function)를 사용할 수 있습니다. 화살표 함수는 this바인딩 과정이 없습니다. 그리고 상위 스코프체 이닝에 의해 this를 참조하게 됩니다.

     

    다만 위의 방법은 몇 가지 단점이 있습니다.

    • 위의 값은 타입스크립트가 검사하지 않은 코드여도 런타임에 정상작동합니다
    • 인스턴스를 생성할 때마다 해당 메서드가 생성되므로 메모리가 낭비됩니다
    • 프로토타입에 정의되지 않은 메서드이므로, super의 사용이 불가능합니다

     

    this parameter

    function fn(this: SomeType, x: number) {
    }
    
    // 컴파일된 코드
    function fn(x) {
      /* ... */
    }

    타입스크립트에서는 this를 매개변수로 사용이 가능하며, 런타임시에는 this는 지워집니다.

     

     

    class MyClass {
      name = "MyClass";
    
      getName (this: MyClass) {
        return this.name;
      }
    }
    
    const c = new MyClass();
    c.getName(); // MyClass
    const gett = c.getName();
    console.log(gett()); // error

     

    위의 getName을 this 매개변수를 이용하여 name이 어떤 클래스의 값인지 명확하게 사용할 수 있습니다.

    다만, 화살표함수에서는 this 매개변수를 사용할 수 없습니다.

     

    this 매개변수를 이용해 조회하는 방법은 위의 화살표함수의 단점과 정반대의 특징을 갖습니다.

    • 클래스의 인스턴스당 하나의 메서드만 할당받습니다
    • super를 사용할 수 있습니다.

     

    this Type

    클래스에서 this 타입은 클래스의 타입을 동적으로 참조합니다.

    class Box {
      contents: string = "";
      set(value: string) { // set(value: string): this
        this.contents = value;
        return this;
      }
    }
    
    class ClearableBox extends Box {
      clear() {
        this.contents = "";
      }
    }
    
    const box = new Box();
    const a = new ClearableBox();
    const b = a.set("hello");
    console.log(box); // { content: '' }
    console.log(b); // { content: 'hello' }

    Box.set의 반환타입은 this로 추론됐습니다.

    ClearableBox에서 set을 호출하여 content의 hello를 전달했지만 Box의 content는 비어있습니다.

    ClearableBox에서 호출했기 때문에 this가 동적으로 Box가 아닌 ClearableBox를 참조했습니다.

     

    class Box {
      content: string = "";
      sameAs(other: this) {
        return other.content === this.content;
      }
    }
     
    class DerivedBox extends Box {
      otherContent: string = "?";
    }
     
    const base = new Box();
    const derived = new DerivedBox();
    derived.sameAs(base); // error

    Box의 sameAs 메서드의 인자는 this타입입니다. Box 이외의 인스턴스가 할당될 수 없습니다.

     

     

    this를 이용한 타입 좁히기

    this와 is 키워드를 이용해 타입을 좁힐 수 있습니다.

    class FileSystemObject {
      isFile(): this is FileRep {
        return this instanceof FileRep; // true
      }
    
      isDirectory(): this is Directory {
        return this instanceof Directory; // true
      }
    
      isNetworked(): this is Networked & this {
        return this.networked; // true
      }
    
      constructor(public path: string, private networked: boolean) {}
    }
    
    class FileRep extends FileSystemObject {
      constructor(path: string, public content: string) {
        super(path, false);
      }
    }
    
    class Directory extends FileSystemObject {
      children: FileSystemObject[];
    }
    
    interface Networked {
      host: string;
    }
    
    const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo");
    
    if (fso.isFile()) {
      fso.content;
    } else if (fso.isDirectory()) {
      fso.children;
    } else if (fso.isNetworked()) {
      fso.host; 
    }

     

    FileSystemObject는 부모클래스로 인스턴스인지 반환하는 메서드를 갖고 있습니다.

    이 메서드들을 자식클래스에서 사용하여 각 자식 클래스의 프로퍼티를 사용하는 논리에 사용됩니다.

    fso는 FileSystemObject 타입을 가진 FileRep의 인스턴스입니다.

     

    fso.isFile은

     

    this instanceof FileRep

     

    FileRep은 FileSystemObject의 인스턴스인가?

     

    라는 구문이 됩니다.

     

    그리고 해당 메서드의 반환하는 타입은 this is FileRep 이므로 true | false입니다.

     

    이를 if문에 사용하면 fso.isFile이 true를 반환하므로 fso는 FileRep으로 좁혀지게 됩니다.

    그러므로 fso의 프로퍼티인 content의 접근할 수 있게 됩니다.

    이런 식으로 this를 이용해 타입을 좁혀 원하는 프로퍼티에 조금 더 명확하게 접근할 수 있습니다.

     

     

    class Box<T> {
      value?: T;
     
      hasValue(): this is { value: T } {
        return this.value !== undefined;
      }
    }
     
    const box = new Box();
    box.value = "Gameboy";
    console.log(box.value); // Box<unknown>.value?: unknown
    console.log(box.hasValue());
    if (box.hasValue()) {
      box.value; // Box<unknown>.value: unknown
    }

    한 가지 예시를 더 보겠습니다.

    Box는 제네릭 클래스입니다. 생성된 인스턴스 'box'는 어떤 타입도 제공하지 않고 초기화했습니다.

    그렇기 때문에 unknown 타입으로 추론됐고 hasValue를 이용해 타입을 좁히지 않았을 때에는 value가 옵셔널타입으로 확인됩니다.

    if문에서 hasValue를 거친 후 에는 옵셔널 타입이 제거된 것을 확인할 수 있습니다.

     

    const box2 = new Box<string>();
    box2.value = "hello"; // Box<string>.value?:string | undefined
    box2.value; // Box<string>.value?:string
    console.log(box2.hasValue()); // true
    if (box2.hasValue()) {
      box2.value; // Box<unknown>.value: string
    }

    만약 제네릭에 타입을 전달하더라도 hasValue를 이용해 타입가드를 하지 않으면 value의 옵셔널은 사라지지 않습니다.

     

     

    매개변수 속성

    class Params {
      constructor(
        public readonly x: number,
        protected y: number,
      ) {
      }
    }
    const a = new Params(1, 2, 3); // error Params의 인자는 2개가 필요합니다
    console.log(a.x);
    console.log(a.z); // error z 는 외부에서 접근할 수 없습니다

    타입스크립트에서는 constructor에 매개변수에 접근제어자를 붙여 바로 멤버변수를 정의할 수 있습니다.

     

     

    클래스 표현식

    const someClass = class<Type> {
      content: Type;
      constructor(value: Type) {
        this.content = value;
      }
    };
     
    const m = new someClass("Hello, world"); // m:someClass<string>

    클래스 표현식은 함수 표현식과 비슷합니다.

    위 예제와 같이 함수표현식을 정의하듯 클래스도 정의할 수 있습니다.

    클래스 표현식은 익명클래스이지만 클래스명을 참조할 때는 정의된 변수명으로 참조할 수 있습니다.

     

     

    abstract Class

    abstact class는 추상클래스입니다. 추상 클래스는 어떤 것도 구현하지 않고 인스턴스를 생성하지 않습니다.

    다만 해당 클래스를 상속하는 자식 클래스가 어떤 것을 구현해야 하는지 알려줍니다.

     

    abstract class Base {
      abstract getName(): string;
     
      printName() {
        console.log("Hello, " + this.getName());
      }
    }
     
    const b = new Base(); // error 추상클래스는 인스턴스할 수 없습니다

    클래스와 메서드 앞에 abstract 키워드를 붙이면 추상클래스가 됩니다.

     

    class Derived extends Base {
      getName() {
        return 'world';
      }
    }
    
    class Derived2 extends Base { // error 추상클래스의 getName을 구현하지않았습니다.
      getAge() {
        return 29;
      }
    }
    
    const d = new Derived();
    d.printName(); // hello world

    위처럼 추상클래스는 추상클래스에 정의된 내용을 구현해 주는 자식클래스가 있어야 하며, 자식클래스는 반드시 추상클래스의 추상메서드를 구현해야 합니다.

     

     

    Abstract Construct Signature

    function greet(ctor: typeof Base) {
      const instance = new ctor(); // error 추상 클래스는 인스턴스를 생성할수없습니다
      instance.printName();
    }
    
    // new 연산자를 사용하여 Base 라는 추상 클래스를 구현하는 메서드를 인자로 받을수있습니다.
    function greet2(ctor: new () => Base) {
      const instance = new ctor();
      instance.printName();
    }
    
    greet2(Derived);
    greet2(Base); // error typeof Base는 new () => Base에 할당 할 수 없습니다

    추상클래스를 인자로 받아 생성하는 함수에서는 위와 같이 typeof 시그니처가 아닌 new () => Base 시그니처를 사용해야 합니다.

     

     

    구조가 비슷한 클래스

    class Point1 {
      x = 0;
      y = 0;
    }
    
    class Point2 {
      x = 0;
      y = 0;
      z = 0;
    }
    
    const p: Point1 = new Point2();
    const p2: Point2 = new Point1(); // error

    Point2는 Point1과 구조가 비슷하며 조금 더 넓은 집합의 클래스입니다.

    x, y 프로퍼티가 동일하기 때문에 Point1의 타입을 가진 Point2의 인스턴스를 생성할 수 있습니다.

    하지만 반대의 경우 Point2 클래스 타입은 'z'가 필수이기 때문에 반대의 경우는 비슷한 타입이 아닙니다.

     

     

    class Empty {}
    
    function fn(x: Empty) {
    }
    
    fn(window);
    fn({});
    fn(fn);

    다만 Empty같이 빈 클래스는 모든 클래스의 상위 집합이기 때문에 어떤 값이던 대입이 가능해지므로 사용하지 않는 게 좋습니다.

    댓글

Designed by Tistory.