Class와 Constructor

제리·2023년 9월 22일
0

마이버추얼트립

목록 보기
4/9
post-thumbnail

지금까지 함수형으로 작성했던 코드를 클래스형으로 전환한다.


Review

타입스크립트로 객체지향 프로그래밍을 이해하면서 클래스에 대한 공부의 필요성을 느꼈다. Vanila JS 프로젝트에도 함수형으로 하고, React로 넘어갔을 때도 클래스형은 이해만 하고 넘어 가느라 따로 다뤄볼 생각을 못했다. 지금은 객체지향을 고려할 단계는 아니지만 ‘객체(Object)’에 대한 이해는 분명 필요했다. 생성자 함수와 클래스로 작성된 코드의 실행 순서를 따라가면서 객체에 대한 개념을 정립했다. 다만 클래스의 뚜렷한 장점이 느껴지게끔 코드를 작성하지 못한 것 같아 아쉽다.

클래스는 객체를 찍어내는 공장

다양한 방법으로 기능을 꼬아보면서 데이터를 묶고 싶고, 상속으로 중복을 제거하고 싶다는 생각을 자연스레 했던 것 같다. 특히 같은 기능을 함수형과 클래스형으로 구현해 보면서 "클래스는 객체다"로 클래스 자체를 객체처럼 받아들였다는 것을 알게 되었다. 클래스는 객체를 만드는 방법 중 하나이다. 이제는 함수로도 만들 수 있고, 클래스로도 만들 수 있는 이러한 객체형 자료를 어떻게 활용할 것인가를 고민해 보게 되었다.

객체는 관련 있는 데이터들을 분류해 묶은 자료

어떤 방법이든 객체를 반환한다는 것은 서로 관련이 있는 것을 담아 만든 상자를 만드는 것과 같다. 그 흐름이 와닿았을 때 도형 맞추기 장난감이 떠올랐다. 여러 가지를 담을 수 있지만, 정해진 규격에 딱 맞거나 또는 크기가 작아서 쏙 들어갈 수 있어야 한다. 내가 받아들인 객체지향 프로그래밍을 표현하기 좋은 도구 같다.

동시에 함께 친해질 수는 없을까

다시 돌아보는 과정은 생각보다 오랜 시간이 소요되었다. 클래스와 친해질 때쯤 상속을 만나 다시 멀어지고, 상속도 친해질 때쯤 객체지향이라며 추상화, 캡슐화, 다형성이 튀어나와 자바스크립트와 내 사이를 이간질했다. 그리고 "이제는 이해되었다!" 할 즈음에 타입스크립트를 다시 건들었다가 자바스크립트와 타입스크립트 둘 다 어색해지는 상황이 발생했다. 정말 다행인 점은 정리를 해나갈수록 앞으로 하게 될 리액트 프로젝트는 너무 기대된다는 것이다. 그 편리함을 이미 알고 있기 때문에 지금의 어려움을 무겁지 않게 받아들일 수 있는 것 같다. 지금의 감을 잃지 않도록 리액트 버전으로 넘어가도 비교해 가면서 구현해 보아야겠다.


Demo

Demo 바로가기


Markup

<main id="section">
    <button type="button" class="section__btn--add">카테고리 추가</button>
</main>

카테고리를 생성할 수 있는 버튼을 추가한다.


Check

  • 카테고리도 사용자가 직접 입력하여 추가할 수 있다.
  • 카테고리가 생성될 때 id를 부여하고 하위 list가 id를 이용해 구분될 수 있도록 한다.
  • 생성되는 Item은 2종류(text, check)로 변경된다.
    • 처음 생성되는 카테고리는 'text'로, 이후 카테고리는 'check'로 고정한다.
  • Item은 ItemComponent를 상속받아 중복 코드를 제거한다.

Contents

01. 클래스의 DOM 생성

class에서 DOM 생성은 생성자 함수 안에서 실행된다. class 버전에서는 app.js 파일이 entry point이므로 App 클래스 생성자 함수 안에 버튼을 선택해 이벤트를 등록하고, 새로운 카테고리와 아이템을 만드는 메서드를 추가했다. 마지막에 new App()으로 호출해준다.

02. 1 카테고리 : 1 리스트 : N 아이템

//section.js
this.section = document.createElement('section');
this.section.className = `section section${categoryId}`;

//list.js
this.ul = document.createElement('ul');
this.ul.className = `section__list list${categoryId} disabled`;

지금까지는 정해진 카테고리 중 선택에 따라 아이템을 추가했지만, 이제는 카테고리를 사용자가 직접 생성할 수 있게 되었다. 그래서 섹션이 새로 추가될 때 식별자가 필요하고, 그 식별자를 리스트와 아이템이 찾아낼 수 있어야 했다.

별도 데이터로 관리하지 않고 DOM에 바로 추가했기때문에 구분이 난감했다. 배열로 구현해보려다가 그렇지 않으면 어떻게 할 수 있을까 고민해보며 클래스에 카테고리를 구분할 수 있는 값을 넣었다. 처음에는 카테고리명을 적용했는데 클래스명에 한글이 표시되는 게 맞는지 고민되어 별도 id를 부여해 그 값을 적용하기로 했다.

03. 사용자가 입력한 카테고리 추가

카테고리를 추가하면 <select>에도 추가되어야 한다. 사용자가 입력한 카테고리명을 <option> 태그의 value에 넣으니 아이템 클래스를 호출하기 위한 분기가 어려워졌다. 02-module에서는 shopping과 packing 2가지로 고정이 되어 있어 그에 따라 분기했지만, 이번에는 카테고리를 생성할 수 있게 되어 별도의 값이 필요했다.

<option> 태그의 value에 categoryId를 할당한다.
구현에 초점을 두고 첫 번째 카테고리는 text형으로, 두 번째 이후부터는 check형을 적용했다. value로 실제값을 예측할 수 없는 것은 좋지 않은 것 같다. 카테고리를 생성할 때, text / check type을 선택할 수 있도록 하고, 카테고리 정보를 객체로 관리할 수 있도록 개선해 보자.

04. 상속을 이용해 중복 코드 제거하기

[참고] 하단 Note - 상속 바로잡기

shopping과 packing 코드는 마크업을 제외하고 코드가 동일하다. 마크업을 변경할 수 있으면서 아이템이 필요한 기능을 모두 가진 클래스를 추상화해 본다.

export class ItemComponent {
    constructor(value) {
        this.value = value;
        this.item = document.createElement('li');
        this.item.className = 'item__container';
        this.item.innerHTML = this.render(this.value);
    }
    render() {}
    editItem(editModeString) {
        this.item.firstElementChild.classList.add('disabled');
        const newData = document.createElement('div');
        newData.className = 'item__form--edit';
        newData.innerHTML = editModeString;
        this.item.append(newData);
    
        ...
    }
    deleteItem() {
        ...
    }
}

Item이 공통으로 가지는 것

  • 사용자의 input 입력값
  • <li class="item__container"> {children} <li>
  • 메서드
    • editItem
    • deleteItem

{children} 안에 어떻게 값을 받아올까 고민하던 차에 리액트 클래스 코드가 생각났다. 그리고 클래스를 상속하게 되면 부모의 생성자 함수가 먼저 실행되는 점을 이용했다. 동적으로 생성한 <li> 태그의 innerHTML을 자식 클래스에서 반환한 리터럴값으로 대체한다.

이때, 자식 클래스(TextList, CheckList)가 호출될 때 받은 사용자의 입력값을 바로 부모 클래스에게 전달해 주었다. 부모의 생성자 함수가 먼저 실행되므로 이때 미리 값을 알고 있어야 DOM 요소가 렌더링 될 때 적용할 수 있기 때문이다.

CheckList 클래스의 constructor에서 value를 초기화한다면 render 메서드가 먼저 실행되기 때문에 변경된 value를 적용할 수 없다.

리액트의 상태(state)가 떠오른다. 리액트와 동일하게 Component 클래스를 활용해 웹컴포넌트를 구해보려고 시도했지만 현재 프로젝트 기능에 적용하기엔 무리가 있었다. 그저 리액트가 그리울 뿐이다.

render(value) {
    return `
        <div class="item">
            <label>
                <input type="checkbox"/>
                <span>${value}</span>
            </label>
            <div>
                <button type="button" class="item__button--edit">수정</button>
                <button type="button" class="item__button--delete">삭제</button>
            </div>
        </div>
    `;
    }
}

Note

✳︎ Class와 Constructor

class 클래스명 {
  //필드 - 클래스가 가지는 정보
  constructor(호출 시 전달 받은 정보){
    this.멤버변수 = 호출 시 전달 받은 정보
  }
  //메서드
}

//사용
const a = new 클래스명(전달할 정보)

생성자(constructor)는 함수이다.

클래스를 하나의 공식처럼 외우다 보니 constructor도 하나의 함수라는 것을 인지하지 못했다. 무슨 자신감으로 클래스를 이해했다고 생각했었을까 싶다.

클래스 호출은 생성자 함수 호출, 그리고 객체 반환

  1. 생성자 함수를 호출하면 클래스의 constructor를 실행한다.
  2. 생성자 함수는 객체값을 초기화한다.
    a. 클래스 호출 시 전달한 데이터들은 constructor에 전달되고, this.name에 대입하여 초기화한다.
  3. 초기화한 객체를 반환한다. 이때 this는 생성된 객체 자신을 가리키게 된다.

✳︎ Class 상속

Class는 extends를 이용해 상속을 구현할 수 있으며 부모 클래스(super)와 자식 클래스(derived)로 표현한다. 자식클래스는 1개의 부모클래스만 상속받을 수 있다.

상속 시 자식 클래스는 따로 작성하지 않아도 constructor가 생성된다.
(this. 부모클래스변수를 사용할 수 있는 이유)

class User extends Person {
    //constructor(... args){
            //super(... args)
    //}
}

자식 클래스의 매개변수

  • 자식 클래스에서도 따로 인자를 받아야 한다면 매개변수 이름을 한 번 더 확인한다.
  • 상속을 받으면 자식 클래스의 constructor에서 super()로 받아줘야 한다.
  • 넘길 값이 없다면 super()를 비워도 된다.

super() 안에 넣는 인자는 순서를 지켜야 한다.

class Person {
  constructor(name, age){
    this.name = name
    this.age = age
  }
}


class Student extends Person {
  constructor(age, job){
    super(age)
    this.job = job
  }
}

const student = new Student(3, 'student')

Student 클래스를 호출하면서 age와 job을 전달했다. super는 age만 전달한다. 이때, student 인스턴스의 값은 { name : 3, job : ‘student’ }가 된다. 만일 자식 클래스에서 this.job으로 초기화를 시켜주지 않으면 student 인스턴스의 값은 { name : 3 }이 된다.


✳︎ 상속 바로잡기

class 하면 ‘붕어빵틀’이라는 예시가 있다. 공통으로 사용되고, 중복될 수 있는 코드를 틀로 만들어두면 동일한 모양의 붕어빵 객체를 만들어낸다. 그리고 자식 클래스가 가져가서 업그레이드할 수도 있다. 여기에서 내가 잘못 받아들인 부분은 부모 클래스는 껍데기 틀이고, 이를 상속한 자식 클래스는 이 틀을 참고한 새로운 틀이라고 생각했다. 글로 적으면서도 이렇게 보면 맞는 말 같다. 코드로 다시 확인해 보자.

class Student extends Person {
    constructor(a, b, job) {
        super(a, b);
        this.job = job;
    }
}

const student = new Student('김둘리', 30, 'student');

출력 결과는 { name: '김둘리', age: 30, job: 'student' }이다.

나는 이 출력의 결과를 { a: ‘김둘리’, b: 30, job: ‘student’ }가 되는 것으로 상속을 이해하고 있었다. 부모 클래스의 name과 관련된 기능들이 자식 클래스의 a로 대체되는 것이라 생각했다. 동일한 이름을 쓰는 것은 선택적 옵션으로 하는, 부모 클래스를 참고한 독립적인 클래스가 생성되는 것처럼 생각한 것이다. 이대로라면 같은 부모 클래스를 상속받아도 자식 클래스들은 서로 다른 멤버변수명으로 동일한 기능을 구현할 수 있게 된다.


super()는 부모 클래스의 생성자 함수 호출이다.

이 말은 곧 super에 전달하는 인자가 부모 클래스 생성자 함수의 인자가 되는 것으로 값을 상속받는 것이 아니라 오히려 보내는 것이다. 다만, super에 담긴 인자의 이름을 어떤 것으로 하더라도 부모 클래스에서 정해진 이름을 따르게 된다. 함수로 생각하면 너무나도 당연한 말이다. 내 식대로 상속을 정의해 본다면 자식 클래스는 부모 클래스에서 선언되고 초기화된 변수를 상속받는다. 즉, key는 부모가 결정하고, value는 자식이 재할당한다. 자식에서 어떤 key값을 적용하던 super에 전달되는 순서에 따라 부모의 key값을 따른다.

글 상단의 Contents - 04를 다시 보자.
사용자의 입력값 value가 super로 전달되어 부모 클래스의 this.value로 초기화된다. 그리고 부모 클래스에서 생성된 this.item을 자식 클래스에서 추가 설정한다.


상속받은 자식 클래스는 정해진 멤버변수의 규격을 사용한다.

이 프로젝트에서 사람과 관련된 기능은 꼭 name과 age를 쓰게끔 정하는 것이다. 따로 재할당 하지 않고, 또는 readonly를 설정하면 값도 정해진대로 사용한다. 만일 내가 처음에 받아들인 대로 자식 클래스만의 독립적인 틀을 만든다면 자유도는 높아진다. 자유로워지는 만큼 규격과 규칙은 없어진다. 어떤 자식은 name을 a로, 어떤 자식은 name을 b로 쓸 수 있다. 붕어빵 기계로 국화빵을 만드는 느낌일까?


예시로 정리하기

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    check = () => {
        console.log(`이름은 ${this.name}`);
        console.log(`나이는 ${this.age}`);
    };
}
    
class Student extends Person {
    constructor(a, b, job) {
        super(a, b);
        this.job = job;
    }
}

const student = new Student('김둘리', 30, 'student');
  1. Student 클래스는 Person 클래스를 상속받는다.
  2. Student 클래스가 생성될 때 a / b / job을 받고, super(a, b)로 Person의 생성자 함수에 전달한다.
  3. super(a, b)는 Person 클래스의 constructor(name, age){}와 같다.
  4. Student 클래스 호출 순서대로 ‘김둘리’와 30은 Person 클래스의 this.name과 this.age에 할당된다.
  5. Student의 인스턴스인 student는 Person을 상속받아 check 메서드에 접근할 수 있고, 할당받은 값으로 출력된다.
profile
DOM과 친해지기

0개의 댓글

관련 채용 정보