개발/JavaScript

[JavaScript] JavaScript에서의 SOLID 원칙

xuwon 2024. 9. 13. 16:19
SOLID 원칙이 뭔데?

SOLID 원칙이란 소프트웨어 설계에서 유지보수성과 확장성을 높이기 위한 다섯 가지 객체 지향 설계 원칙이다.
말 그대로, 좋은 코드를 만들기 위한 원칙!

이 원칙들은 잘 구조화된 소프트웨어를 작성하는 데 중점을 두며, 코드의 가독성, 유연성, 테스트 가능성 등을 향상시키는 데 기여한다!

 

객체 지향 설계 원칙...?
자바스크립트는 완전한 객체 지향 언어가 아닌데?

 

그렇다.
JavaScript는 전통적인 객체 지향 언어인 Java, C++과는 다르게 프로토타입 기반 객체 지향 언어이다.

 

그럼 프로토타입 기반 OOP와 클래스 기반 OOP는 뭐가 다를까?

특징 클래스 기반 OOP 프로토타입 기반 OOP
객체 생성 방식 클래스를 정의하고,
클래스를 통해 객체 생성
기존 객체를 복사하거나,
object.create()로 생성
상속 방식 클래스 상속을 통해
상속 관계 형성
프로토타입 체인을 통해
객체가 다른 객체를 상속
동적 속성 추가 객체 생성 후 속성이나
메소드 추가 어려움
생성 후에도 객체에 속성 및
메소드를 동적으로 추가 가능
설계 시점 컴파일 시점에
클래스 구조가 고정됨
런타임 시점에 객체가
동적으로 수정 가능
유연성 구조가 고정적이며 명확함 유연하고 동적으로
변할 수 있음


클래스 기반 OOP는 객체가 클래스를 기반으로 생성되고, 상속은 클래스 간에 이루어지고,
명확한 상속 구조가 특징이다.
프로토타입 기반 OOP는 객체가 다른 객체로부터 상속되며, 객체를 동적으로 수정할 수 있는 유연함이 특징이다.

 

 

심지어 자바스크립트는 객체 지향뿐만 아니라 함수형 프로그래밍 스타일도 지원하는 다중 패러다임 언어이다.
ES6 이후로는 함수형 프로그래밍 패턴이 더욱 강조되었다고...

 

함수형 프로그래밍과 객체 지향 프로그래밍은 뭐가 다를까?

특징 함수형 프로그래밍 (FP) 객체 지향 프로그래밍 (OOP)
중심 개념 함수, 순수 함수, 불변성 객체, 클래스, 상속, 상태
상태 관리 상태가 없으며, 불변성 유지 객체 상태가 변경될 수 있음
데이터와 행동 데이터와 행동을 분리,
데이터는 불변
데이터와 행동(메소드)을
객체 내부에 함께 정의
코드 구조 함수의 조합을 통해 문제 해결 객체 간의 상호작용을 통해
문제 해결
부수 효과 (Side effects) 가능한 부수 효과를 피함 부수 효과 발생 가능


함수형 프로그래밍은 함수와 불변성을 강조하고, 객체 지향 프로그래밍은 객체와 상태를 강조한다고 생각하면 될 것 같다.

 

자바스크립트는 동적이며 프로토타입 기반 언어이지만,
SOLID 원칙을 적용하여 더 유연하고 유지보수하기 쉬운 코드를 작성할 수 있다.

SOLID 원칙은 주로 객체 지향 프로그래밍(OOP)에 적용되지만,
자바스크립트와 같은 언어에서도 클래스, 함수, 모듈 단위로 이를 적용할 수 있다.

 

 

각 원칙을 자바스크립트에서 어떻게 적용할 수 있는지 살펴보자.

 


 

1. 단일 책임 원칙 (Single Responsibility Principle, SRP)

하나의 함수는 단 하나의 책임을 가져야 한다!


함수는 하나의 목적만 가져야 한다!
만일 함수가 여러가지 기능을 수행하고 있다면, 해당 기능들을 각각의 함수로 분리하는 것이 좋다.

 

// SRP 위반 - 여러 역할을 하는 함수

function handleUser(user) {
  // 사용자 인증
  authenticateUser(user);
  // 사용자 데이터 저장
  saveUserData(user);
}


// SRP 준수 - 각 기능을 별도로 분리

function authenticateUser(user) {
  // 사용자 인증 로직
}

function saveUserData(user) {
  // 사용자 데이터 저장 로직
}

 

2. 개방-폐쇄 원칙 (Open/Closed Principle, OCP)

함수는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있어야 한다.

즉, 새로운 기능을 추가할 때 기존 코드를 수정하지 않고 확장할 수 있어야 한다.

 

// OCP 위반 - 기능을 확장하기 위해 기존 코드를 수정해야 함
// type이 달라지면 getDiscount()를 수정해야 함!

function calculateDiscount(type, price) {
  if (type === 'regular') return price * 0.1;
  if (type === 'vip') return price * 0.2;
}


// OCP 준수 - 확장 가능한 구조로 변경

const discountStrategies = {
  regular: price => price * 0.1,
  vip: price => price * 0.2,
};

function calculateDiscount(type, price) {
  return discountStrategies[type](price);
}

새로운 할인 전략을 추가할 때 기존 함수 calculateDiscount를 수정하지 않고, discountStrategies 객체에 추가할 수 있다.

 

3. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

함수가 서로 교체 가능해야 하며, 교체된 함수가 기존 함수처럼 동일하게 동작해야 한다.

 

// LSP 위반: birdFly 와 penguinFly 는 서로 대체 불가능하다.

function birdFly() {
  console.log("Flying");
}

function penguinFly() {
  throw new Error("Penguins can't fly");
}


// LSP 준수: penguinMove와 birdMove는 서로 다르게 동작하지만, 호출 방식은 동일하다.

function birdMove() {
  console.log("Flying");
}

function penguinMove() {
  console.log("Swimming");
}

function makeMove(animalMove) {
  animalMove();
}

makeMove(birdMove);    // "Flying"
makeMove(penguinMove); // "Swimming"

 

4. 인터페이스 분리 원칙 (Interface Segregation Principle, ISP)

클라이언트는 자신이 사용하지 않는 함수에 의존하지 않아야 한다.

큰 함수보다는 작은 함수로 나누어 자신이 사용하지 않는 메서드에 의존하지 않도록 해야 한다!

 

// ISP 위반: 하나의 함수에 너무 많은 책임이 있음

function operate(vehicle) {
  vehicle.drive();
  vehicle.fly();
}



// ISP 준수: 각각의 기능을 분리된 함수로 만듦

function drive(vehicle) {
  vehicle.drive();
}

function fly(vehicle) {
  if (vehicle.fly) vehicle.fly();
}

const car = { drive: () => console.log("Driving") };
const plane = { drive: () => console.log("Taxiing"), fly: () => console.log("Flying") };

drive(car);   // "Driving"
drive(plane); // "Taxiing"
fly(plane);   // "Flying"

drive와 fly를 분리함으로써, 자동차와 비행기를 각각 처리하는 함수가 필요로 하는 기능만 사용하도록 분리했다.

 

얼핏 보면 단일 책임 원칙이랑 인터페이스 분리 원칙이랑 비슷해 보이지만,

SRP개별 클래스나 함수의 역할을 제한하는 데 초점을 맞추며, 하나의 책임을 가지도록 설계한다.
ISP인터페이스를 작게 분리하여 클라이언트가 필요하지 않은 메서드에 의존하지 않게 설계하는 데 중점을 둔다.

이러한 차이점이 있다.

 

 

 

5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)

함수는 구체적인 구현이 아닌, 추상화에 의존해야 한다.

 

// DIP 위반: switchOperate 함수가 구체적인 lightBulbOn 함수에 직접 의존.

function lightBulbOn() {
  console.log("Light is on");
}

function switchOperate(bulb) {
  bulb();
}

switchOperate(lightBulbOn); // "Light is on"


// DIP 준수: Switch가 추상 클래스 Device에 의존하여 유연성을 확보.

function operateDevice(device) {
  device();
}

function lightBulbOn() {
  console.log("Light is on");
}

operateDevice(lightBulbOn); // "Light is on"

 

SOLID 원칙을 따르면 다음과 같은 장점이 있다.

- 유지보수성: 각 함수가 명확한 책임을 가지기 때문에 유지보수가 용이하다.
- 확장성: 새로운 기능을 추가하거나 기존 기능을 확장할 때 기존 코드를 수정할 필요가 없다.
- 테스트 용이성: 각 기능이 독립적이므로 테스트 작성이 더 쉬워진다.
- 재사용성: 모듈들이 독립적으로 작동하므로, 재사용성이 높아진다.

따라서 코드를 작성할 때 SOLID 원칙을 생각하며 작성하면 좋을 것 같다.

솔직히 내용이 너무 어려워서... 잘 적은 건지 모르겠다. ㅠㅠ

 

 


 


그러면 이번에는, 이전에 적은 블로그인 

2024.09.11 - [개발 공부] - [JavaScript] 허접한 몬스터 때려잡기 게임 만들기

 

[JavaScript] 허접한 몬스터 때려잡기 게임 만들기

현재 JavaScript의 기초 부분을 학습 중이다.document.createElement() 메소드를 활용하여 몬스터 잡기 게임을 만들어 보았다. 우선 HTML 코드는 이렇게 작성해 보았다. container 의 style은 추후에 작성하겠지

xuwon.tistory.com

해당 글의 코드를 SOLID 원칙 중 단일 책임 원에 따라 고쳐보도록 하겠다!

 

const container = document.getElementById("container");

// 몬스터 모양 만들기
const monster = document.createElement("div");
monster.textContent = "*_*";
monster.style.width = "50px";
monster.style.height = "50px";
monster.style.backgroundColor = "rgb(255, 158, 158)";
monster.style.display = "flex";
monster.style.justifyContent = "center";
monster.style.alignItems = "center";
monster.style.position = "absolute";
monster.style.pointerEvents = "all"; // div 전체가 클릭 가능하게 만듦

container.appendChild(monster);

// 화면 크기
const screenWidth = container.offsetWidth;
const screenHeight = container.offsetHeight;

// 몬스터의 이동 방향 및 속도
let xSpeed = Math.random() * 6 + 1; // X축 속도
let ySpeed = Math.random() * 6 + 1; // Y축 속도
let xPosition = 0; // 초기 X 위치
let yPosition = 0; // 초기 Y 위치

function moveMonster() {
    xPosition += xSpeed;
    yPosition += ySpeed;

    // X축 경계에 도달했을 때 반대 방향으로 전환
    if (xPosition <= 0 || xPosition >= screenWidth - 50) {
        xSpeed = -xSpeed; // 속도 반전
    }

    // Y축 경계에 도달했을 때 반대 방향으로 전환
    if (yPosition <= 0 || yPosition >= screenHeight - 50) {
        ySpeed = -ySpeed; // 속도 반전
    }

    // 몬스터의 위치 업데이트
    monster.style.left = xPosition + "px";
    monster.style.top = yPosition + "px";
}

// 50ms마다 몬스터 이동
setInterval(moveMonster, 40);

// 입력 받기
let monsterHP = 100;
let attackDamage = parseInt(prompt("1회 공격시 데미지는? (양의 정수)"));
let attackCount = 0;

// 이벤트 리스너 함수 설정
const attackFunction = () => {
    attackCount++;
    monsterHP -= attackDamage;
    console.log(monsterHP, attackCount);

    const attackText = document.createElement("p");
    attackText.textContent = `Attack! : ${-attackDamage}`;
    container.appendChild(attackText);
    attackText.style.color = "red";


    if (monsterHP <= 0) {
        container.removeChild(attackText);
        container.removeChild(monster);

        const h2 = document.createElement("h2");
        h2.textContent = `몬스터 잡기 완료! 수고하셨습니다.`;
        h2.style.color = "green";
        container.appendChild(h2)
    } else {
        setTimeout(() => {
            container.removeChild(attackText);
        }, 300); // 0.3초 뒤 삭제
    }
}

if (attackDamage > 0) {
    monster.addEventListener('click', attackFunction);
} else {
    alert("잘못 입력하셨습니다. 게임을 취소합니다.")
    container.removeChild(monster);
    container.style.border = "none";
}

 

우선 가장 먼저 눈에 보이는 것은, moveMonster() 이다.
몬스터의 위치를 바꾸는 것 뿐만 아니라, 경계에 부딪힌 경우까지 정의하고 있다.


// 몬스터의 이동 방향 및 속도
let xSpeed = Math.random() * 6 + 1; // X축 속도
let ySpeed = Math.random() * 6 + 1; // Y축 속도
let xPosition = 0; // 초기 X 위치
let yPosition = 0; // 초기 Y 위치

function moveMonster() {
    xPosition += xSpeed;
    yPosition += ySpeed;

    // X축 경계에 도달했을 때 반대 방향으로 전환
    if (xPosition <= 0 || xPosition >= screenWidth - 50) {
        xSpeed = -xSpeed; // 속도 반전
    }

    // Y축 경계에 도달했을 때 반대 방향으로 전환
    if (yPosition <= 0 || yPosition >= screenHeight - 50) {
        ySpeed = -ySpeed; // 속도 반전
    }

    // 몬스터의 위치 업데이트
    monster.style.left = xPosition + "px";
    monster.style.top = yPosition + "px";
}

// 50ms마다 몬스터 이동
setInterval(moveMonster, 40);


이걸 먼저 단일 책임 원칙에 따라 바꿔보도록 하자.

// 몬스터의 이동 방향 및 속도
let xSpeed = Math.random() * 6 + 1; // X축 속도
let ySpeed = Math.random() * 6 + 1; // Y축 속도
let xPosition = 0; // 초기 X 위치
let yPosition = 0; // 초기 Y 위치

// 경계에 부딪힐 경우
function monsterXBoundary(xPosition) {
    // X축 경계에 도달했을 때 반대 방향으로 전환
    if (xPosition <= 0 || xPosition >= screenWidth - 50) {
        xSpeed = -xSpeed; // 속도 반전
    }

    return xSpeed;
}

function monsterYBoundary(yPosition) {
    // Y축 경계에 도달했을 때 반대 방향으로 전환
    if (yPosition <= 0 || yPosition >= screenHeight - 50) {
        ySpeed = -ySpeed; // 속도 반전
    }

    return ySpeed;
}

function monsterBoundary (xPosition, yPosition) { // x, y축 경계 확인 함수
    monsterXBoundary(xPosition);
    monsterYBoundary(yPosition);
}

// 몬스터 위치 업데이트 함수
function monsterPositionUpdate() {
    xPosition += xSpeed;
    yPosition += ySpeed;

    // 몬스터의 위치 업데이트
    monster.style.left = xPosition + "px";
    monster.style.top = yPosition + "px";
}

function moveMonster() {
    monsterPositionUpdate();
    monsterBoundary(xPosition, yPosition);
}

// 50ms마다 몬스터 이동
setInterval(moveMonster, 40);

 

monsterBoundary 함수는 경계 확인을 하고, monsterPositionUpdate 함수는 경계에 부딪혔을 때를 체크하고,
마지막으로 moveMonster 함수는 두 함수를 호출하는 것으로 수정하였다.

 

두번째로 수정할 것은... 이벤트 리스너 함수!
이것도 몬스터 공격하는 거랑 텍스트 띄우는 거랑 죽었을 때... 이렇게 여러가지 경우에서의 기능들을 다 정의하고 있다.

// 입력 받기
let monsterHP = 100;
let attackDamage = parseInt(prompt("1회 공격시 데미지는? (양의 정수)"));

// 이벤트 리스너 함수 설정
const attackFunction = () => {
    monsterHP -= attackDamage;
    console.log(monsterHP, attackCount);

    const attackText = document.createElement("p");
    attackText.textContent = `Attack! : ${-attackDamage}`;
    container.appendChild(attackText);
    attackText.style.color = "red";


    if (monsterHP <= 0) {
        container.removeChild(attackText);
        container.removeChild(monster);

        const h2 = document.createElement("h2");
        h2.textContent = `몬스터 잡기 완료! 수고하셨습니다.`;
        h2.style.color = "green";
        container.appendChild(h2)
    } else {
        setTimeout(() => {
            container.removeChild(attackText);
        }, 300); // 0.3초 뒤 삭제
    }
}

이것 역시 단일 책임 원칙에 따라 분리해보도록 하겠다.

// 입력 받기
let monsterHP = 100;
let attackDamage = parseInt(prompt("1회 공격시 데미지는? (양의 정수)"));

// monster 피 깎기
function monsterAttack(monsterHP) {
    monsterHP -= attackDamage;

    return monsterHP;
}

// attack 메시지 띄우기
function attackMessage() {
    const attackText = document.createElement("p");
    attackText.textContent = `Attack! : ${-attackDamage}`;
    container.appendChild(attackText);
    attackText.style.color = "red";

    return attackText;
}

// Clear 메시지 띄우기
function clearMessage() {
    const h2 = document.createElement("h2");
    h2.textContent = `몬스터 잡기 완료! 수고하셨습니다.`;
    h2.style.color = "green";
    container.appendChild(h2)

    return h2;
}

// 몬스터 죽었을 때 처리
function handleMonsterDeath(monsterHP, attackText, monster) {
    if (monsterHP <= 0) {
        container.removeChild(attackText);
        container.removeChild(monster);

        clearMessage();
    } else {
        setTimeout(() => {
            container.removeChild(attackText);
        }, 300); // 0.3초 뒤 삭제
    }
}

// 이벤트 리스너 함수 설정
const attackFunction = () => {
    monsterHP = monsterAttack(monsterHP);
    const attackText = attackMessage();
    handleMonsterDeath(monsterHP, attackText, monster); // clearText() 안에 포함
}

- monsterAttack() 는 체력을 깎는 역할.
- attackMessage() 는 공격 메시지를 생성.
- clearMessage() 는 몬스터가 죽었을 때 완료 메시지를 출력.
- handleMonsterDeath() 는 몬스터가 죽었는지 확인하고, 그에 따라 메시지를 처리.

그리고 이벤트 리스너 함수에서 한 번에 처리해 주었다.
수정하다가 느낀 건데... 값 업데이트와 매개변수를 신경쓰도록 하자. ㅠㅠ

채찍피티한테 단일 책임 원칙 만족하냐고 물어봤는데,
handleMonsterDeath() 에서 몬스터 죽었는지 확인하는 거랑 메시지 처리하는 거랑 두가지 역할 한다고 고쳐주랬다.
(와 빡세네... 생각 못했다.)

// 몬스터 죽음 확인
function checkMonsterDeath (monsterHP) {
    return monsterHP <= 0;
}

// 몬스터 죽었을 때 처리
function handleMonsterDeath(monsterHP, attackText, monster) {
    if (checkMonsterDeath(monsterHP)) {
        container.removeChild(attackText);
        container.removeChild(monster);

        clearMessage();
    } else {
        setTimeout(() => {
            container.removeChild(attackText);
        }, 300); // 0.3초 뒤 삭제
    }
}

그래서 이렇게 고쳐주었다.

그랬는데 handleMonsterDeath() 에서 몬스터 죽은 거 확인이랑 처리까지 다 해주고 있다고...
아니 이렇게까지 쪼개야 하는 거구나...

그래서
1. 몬스터 죽었을 때 처리 (attackText, monster 삭제)
2. 몬스터 처치 시 attackText 0.3초 뒤 삭제 처리
3. 몬스터 상태 관리

이렇게 3가지로 또 나누어 주었다.
용어도 어떻게 써야할지 모르겠다... 하도 쪼개니 헷갈림.

// 몬스터 죽음 처리 (attackText, monster 삭제)
function handleMonsterDeath (attackText, monster) {
    container.removeChild(attackText);
    container.removeChild(monster);

    clearMessage();
}

// 몬스터 처치 시 attackText 삭제
function removeAttackText (attackText) {
    setTimeout(() => {
        container.removeChild(attackText);
    }, 300); // 0.3초 뒤 삭제
}

// 몬스터 상태 관리
function handleMonsterState(monsterHP, attackText, monster) {
    if (checkMonsterDeath(monsterHP)) {
        handleMonsterDeath(attackText, monster);
    } else {
        removeAttackText(attackText);
    }
}

 

 


 

으하하... 이렇게 단일 책임 원칙에 따라 코드를 수정해 보았다.

최종 코드!! 함수가 아주 많아졌다.
앞으로는 SOLID 원칙을 생각하며 좋은 코드를 작성하길 바라며,,, 이 글을 마친다.

const container = document.getElementById("container");

// 몬스터 모양 만들기
const monster = document.createElement("div");
monster.textContent = "*_*";
monster.style.width = "50px";
monster.style.height = "50px";
monster.style.backgroundColor = "rgb(255, 158, 158)";
monster.style.display = "flex";
monster.style.justifyContent = "center";
monster.style.alignItems = "center";
monster.style.position = "absolute";
monster.style.pointerEvents = "all"; // div 전체가 클릭 가능하게 만듦

container.appendChild(monster);

// 화면 크기
const screenWidth = container.offsetWidth;
const screenHeight = container.offsetHeight;

// 몬스터의 이동 방향 및 속도
let xSpeed = Math.random() * 6 + 1; // X축 속도
let ySpeed = Math.random() * 6 + 1; // Y축 속도
let xPosition = 0; // 초기 X 위치
let yPosition = 0; // 초기 Y 위치

// 경계에 부딪힐 경우
function monsterXBoundary(xPosition) {
    // X축 경계에 도달했을 때 반대 방향으로 전환
    if (xPosition <= 0 || xPosition >= screenWidth - 50) {
        xSpeed = -xSpeed; // 속도 반전
    }

    return xSpeed;
}

function monsterYBoundary(yPosition) {
    // Y축 경계에 도달했을 때 반대 방향으로 전환
    if (yPosition <= 0 || yPosition >= screenHeight - 50) {
        ySpeed = -ySpeed; // 속도 반전
    }

    return ySpeed;
}

function monsterBoundary(xPosition, yPosition) { // x, y축 경계 확인 함수
    monsterXBoundary(xPosition);
    monsterYBoundary(yPosition);
}

// 몬스터 위치 업데이트 함수
function monsterPositionUpdate() {
    xPosition += xSpeed;
    yPosition += ySpeed;

    // 몬스터의 위치 업데이트
    monster.style.left = xPosition + "px";
    monster.style.top = yPosition + "px";
}

function moveMonster() {
    monsterPositionUpdate();
    monsterBoundary(xPosition, yPosition);
}

// 50ms마다 몬스터 이동
setInterval(moveMonster, 40);

// 입력 받기
let monsterHP = 100;
let attackDamage = parseInt(prompt("1회 공격시 데미지는? (양의 정수)"));

// monster 피 깎기
function monsterAttack(monsterHP) {
    monsterHP -= attackDamage;

    return monsterHP;
}

// attack 메시지 띄우기
function attackMessage() {
    const attackText = document.createElement("p");
    attackText.textContent = `Attack! : ${-attackDamage}`;
    container.appendChild(attackText);
    attackText.style.color = "red";

    return attackText;
}

// Clear 메시지 띄우기
function clearMessage() {
    const h2 = document.createElement("h2");
    h2.textContent = `몬스터 잡기 완료! 수고하셨습니다.`;
    h2.style.color = "green";
    container.appendChild(h2)

    return h2;
}

// 몬스터 죽음 확인
function checkMonsterDeath (monsterHP) {
    return monsterHP <= 0;
}

// 몬스터 죽음 처리
function handleMonsterDeath (attackText, monster) {
    container.removeChild(attackText);
    container.removeChild(monster);

    clearMessage();
}

// 몬스터 처치 시 attackText 삭제
function removeAttackText (attackText) {
    setTimeout(() => {
        container.removeChild(attackText);
    }, 300); // 0.3초 뒤 삭제
}

// 몬스터 처리
function handleMonsterState(monsterHP, attackText, monster) {
    if (checkMonsterDeath(monsterHP)) {
        handleMonsterDeath(attackText, monster);
    } else {
        removeAttackText(attackText);
    }
}

// 이벤트 리스너 함수 설정
const attackFunction = () => {
    monsterHP = monsterAttack(monsterHP);
    const attackText = attackMessage();
    handleMonsterState(monsterHP, attackText, monster); // clearText() 안에 포함
}

if (attackDamage > 0) {
    monster.addEventListener('click', attackFunction);
} else {
    alert("잘못 입력하셨습니다. 게임을 취소합니다.")
    container.removeChild(monster);
    container.style.border = "none";
}

 

 

 

참고 자료

- H43RO, "SOLID 원칙, 어렵지 않다!", https://velog.io/@haero_kim/SOLID-%EC%9B%90%EC%B9%99-%EC%96%B4%EB%A0%B5%EC%A7%80-%EC%95%8A%EB%8B%A4
- teo.v, "Javascript에서도 SOLID 원칙이 통할까?", https://velog.io/@teo/Javascript%EC%97%90%EC%84%9C%EB%8F%84-SOLID-%EC%9B%90%EC%B9%99%EC%9D%B4-%ED%86%B5%ED%95%A0%EA%B9%8C