CS/프로그래밍

[CS Study] 디자인 패턴

고래강이 2024. 2. 4. 12:32

정의

소프트웨어를 개발하는 과정의 반복되는 일반적인 문제들에 대해 기준이 되는 해결책을 제공하는 중요한 개념으로 소프트웨어의 특정 구현을 직접 제공하지는 않지만, 반복되는 문제 상황들을 최적화된 방법으로 해결하도록 돕는 컨셉들이다.

 

 

⚠️ 주의사항

  • 모든 문제에 대해서 적용할 수 있는 묘책이 아니다.
  • 억지로 적용하려고 하면 안된다.
  • 과도하게 고민하지마라.
  • 올바른 방식으로 사용하고 있는지에 대해서 고민해라.

Hooks 패턴

전통적인 디자인 패턴들이 Hooks로 다수 교체될 수 있게 됨에 따라서 생명주기와 관련된 메서드를 사용하지 않게 되었다. Hooks가 디자인 패턴이 아닐 순 있지만 React에서 중요한 역할을 한다.
  • state, effect, 기타 커스텀 훅을 통해서 class에서 사용한던 것보다 편리하게 이용할 수도 있는 hooks
  • 복잡한 컴포넌트를 단순화하여 상태가 있는 로직을 언제든 분리하여 사용할 수 있어 관심사 분리 및 재사용성도 증가하며 클래스 없이 React의 기능을 사용할 수 있게 해준다.

 

단점

  • 규칙에 맞춰서 사용을 해야하며 올바르게 사용하기 위한 어느정도의 규칙이 존재한다.
  • useCallback과 useMemo 등 잘못 사용하는 경우도 있기에 사용에 주의를 해야한다.

 

 

HOC 패턴

종종 여러 컴포넌트가 같은 로직을 사용해야하는 경우가 있고 이러한 경우에 재사용하는 방법 중 하나로 다른 컴포넌트를 인자로 넘겨 받는 컴포넌트인 고차 컴포넌트 패턴(HOC)을 사용한다.
  • 여러 컴포넌트에 동일한 스타일을 적용하고 싶을 때 일일히 스타일에 대해서 전달해주는 것이 아닌 HOC패턴을 적용하여 동일한 스타일을 적용하게 할 수 있다.
function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />
  }
}

const Button = () = <button>Click me!</button>
const Text = () => <p>Hello World!</p>

const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)

 

<withAuth>
  <withLayout>
    <withLogging>
      <Component />
    </withLogging>
  </withLayout>
</withAuth>
  • 위와 같이 트리가 깊어지는 상황이 생길 수 있어 hooks를 통해서 HOC패턴을 대신 할 수 있다.

 

장점

  • 한 곳에서 구현한 로직을 여러 컴포넌트에 재사용할 수 있고 해당 로직을 한 곳에서 관리하여 관심사 분리 및 유지 보수에도 좋은 성능을 보인다

 

단점

  • props의 이름이 겹칠 경우에는 병합을 통해서 해결을 해야하는데 이 경우에는 더 복잡한 로직이될 수 있다.
  • 깊이가 깊어질수록 가독성이 떨어지게 된다. 
  • 여러번의 조합으로 병합된 props의 구조를 파악하기 힘들어 디버깅에 방해가 될 때가 있다.

 

 

Observer 패턴

특정 객체를 구독할 수 있는데 이때 구독하는 주체를 Observer라고 하고 이 Observer를 활용해서 Subscriber에게 이벤트 발생을 알린다.
  • 단순히 무한스크롤 시에만 사용되는 것이 아닌 토글에 대해서 알림을 띄운다거나 하는 여러 방면에서 사용될 수 있음

 

주요 개념

  • observers: 이벤트가 발생할 때마다 전파할 Observer들의 배열 
  • subscribe(): Observer를 obserers 배열에 추가한다.
  • unsubscribe(): Observer를 observers 배열에서 제거한다.
  • notify(): 등록된 모든 Observer들에게 이벤트를 전파한다.

 

 복잡해지면 모든 Observer들에 알림을 전파하는데 성능 이슈가 발생할 수 있어 최대한 단순하게 사용하는 것이 좋겠다. (막 여러개 연결하는 방식도 예제에 있어서 그런 것 같음)

 

 

 

Compound 패턴

개발을 하다보면 종종 서로를 참조하는 컴포넌트를 만들기도 한다. 이러한 하나의 작업을 위해 여러 컴포넌트를 만들어 역할을 분담하게 하는 패턴이다.
  • 예시를 보면서 이해를 하면 이해하기 편하다. 특히 호출 시를 자세히 보자
// 선언 시
const FlyOutContext = createContext()

function FlyOut(props) {
  const [open, toggle] = useState(false)

  return (
    <FlyOutContext.Provider value={{ open, toggle }}>
      {props.children}
    </FlyOutContext.Provider>
  )
}

function Toggle() {
  const { open, toggle } = useContext(FlyOutContext)

  return (
    <div onClick={() => toggle(!open)}>
      <Icon />
    </div>
  )
}

function List({ children }) {
  const { open } = useContext(FlyOutContext)
  return open && <ul>{children}</ul>
}

function Item({ children }) {
  return <li>{children}</li>
}

FlyOut.Toggle = Toggle
FlyOut.List = List
FlyOut.Item = Item


// 호출 시
import React from 'react'
import { FlyOut } from './FlyOut'

export default function FlyoutMenu() {
  return (
    <FlyOut>
      <FlyOut.Toggle />
      <FlyOut.List>
        <FlyOut.Item>Edit</FlyOut.Item>
        <FlyOut.Item>Delete</FlyOut.Item>
      </FlyOut.List>
    </FlyOut>
  )
}
  • 하나의 컴포넌트를 구성하기 위해 여러개의 동작을 필요로 하며 굳이 재사용을 하지 않고 이 컴포넌트 하나에서만 쓰일 경우에 이런식으로 구성하면 좋을 것 같다

 

장점

  • 일일히 import해서 사용할 필요가 없으며 내부적으로 가지고 있는 것을 드러내지 않는다.

 

 

Proxy 패턴

일반적으로 대리인을 뜻하는 Proxy는 해당 개개체를 직접 다루는 것이 아니라 Proxy 객체와 인터렉션을 하게 되는 것을 말한다 즉, 대상 객체에 대하여 읽기 및 쓰기를 직접 제어한다.
  • 아래 예제를 보면서 같이 이해를 해보자
const person = {
  name: "John Doe",
  age: 42,
  nationality: "American"
};

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${obj[prop]}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    obj[prop] = value;
    return true;
  }
});

personProxy.name;
personProxy.age = 43;
  • 예제를 보면 알 수 있듯이 personProxy라는 porxy를 생성하여 get, set 메서드를 통해서 person 객체의 값을 가져와 사용하거나 내부에 값을 수정하거나 추가하는 모습을 보인다.
  • 직접적으로 person[name] = 'Jane Doe'라고 수정할수도 있지만 이러한 방식을 택해서 수정할 수도 있다.
  • 어떠한 이점이 있기에 이런 방식을 채택하는걸까?

적용 시기

  • 유효성 검사를 구현할 때 유용하게 사용된다. 사용자는 person 객체의 age 프로퍼티를 문자열로 수정하거나, name 프로퍼티를 빈 문자열로 초기화하거나 할 수 없으며 객체에 존재하지 않는 프로퍼티에 접근할 수도 없다. 이러한 사용자의 요청에 대해 알려줄 수 있다.
  • 객체를 실수로 수정하는 것을 예방해주어 데이터를 안전하게 관리할 수 있게 한다.
const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    if (!obj[prop]) {
      console.log(
        `Hmm.. this property doesn't seem to exist on the target object`
      )
    } else {
      console.log(`The value of ${prop} is ${obj[prop]}`)
    }
  },
  set: (obj, prop, value) => {
    if (prop === 'age' && typeof value !== 'number') {
      console.log(`Sorry, you can only pass numeric values for age.`)
    } else if (prop === 'name' && value.length < 2) {
      console.log(`You need to provide a valid name.`)
    } else {
      console.log(`Changed ${prop} from ${obj[prop]} to ${value}.`)
      obj[prop] = value
    }
  },
})

 

더보기

Reflect

const person = {
  name: "John Doe",
  age: 42,
  nationality: "American"
};

const personProxy = new Proxy(person, {
  get: (obj, prop) => {
    console.log(`The value of ${prop} is ${Reflect.get(obj, prop)}`);
  },
  set: (obj, prop, value) => {
    console.log(`Changed ${prop} from ${obj[prop]} to ${value}`);
    return Reflect.set(obj, prop, value);
  }
});

personProxy.name;
personProxy.age = 43;
personProxy.name = "Jane Doe";
  • 자바스크립트는  Reflect라는 빌트인 객체를 제공하는데 Proxy와 함께 사용해서 대상 객체를 쉽게 조작할 수 있게 해준다 

 

Provider 패턴

앱 내에 여러 컴포넌트들이 데이터를 사용할 수 있게 해야하는 상황이 있다. (예를 들어 theme data를 통해 다크모드 설정이라던가) 이 때 상위 컴포넌트에서 하위로 전달할 때 porps drilling과 같은 안티 패턴을 사용하지 않고서 여러 자식 컴포넌트 간에 데이터를 공유할 수 있게 해주는 패턴이다.
  • 길게 말할 것도 없이 Context를 사용한 방식이다.
  • 적용하고 싶은 컴포넌트의 상단에 Provider로 감싸준 뒤 데이터를 공유하도록 만드는 것이다.

 

단점

  • 상단에 Privider로 감싼 구조를 가지기 때문에 Provider로 제공하는 data의 변화로 인해 리랜더링 시 하위 컴포넌트 모두 리랜더링 되어 성능적으로 최적화에 대한 노력을 강구해야한다. (이러한 이유로 contextAPI가 성능 최적화를 따로 해주어야 해서 귀찮음)

 

 

Prototype 패턴

자바스크립트 객체의 기본 속성인 Prototype을 이용한 패턴으로 동일 타입의 여러 객체들이 프로퍼티를 공유할 때 유용하게 사용할 수 있다. 동일 타입의 여러 객체들이 프로퍼티를 공유하고 해당 객체에 프로퍼티를 직접 선언하지 않아도 되서 메모리를 절약할 수 있다.
  • 객체에 없는 프로퍼티에 접근하려는 경우 자바스크립트는 이 프로퍼티가 나타날  때까지 prototype chain을 거슬러 올라간다. 
class Dog {
  constructor(name) {
    this.name = name;
  }

  bark() {
    return `Woof!`;
  }
}

const dog1 = new Dog("Daisy");
const dog2 = new Dog("Max");
const dog3 = new Dog("Spot");

Dog.prototype.play = () => console.log("Playing now!");

dog1.play();
  • 이런식으로 프로토타입을 통해 추가할 수도 있다.

 

 

Module 패턴

코드 베이스가 커질수록 코드를 유지 보수하기 좋게 쪼개는 것이 중요해지며 모듈 패턴은 이 떄 코드들을 재사용 가능하면서도 작게 나눌 수 있게 해준다. 또한 특정 변수들을 private하게 할 수 있어 전역 스코프의 변수들과의 충돌 문제를 줄일 수 있다.
  • export를 통해서 다른 파일에서도 사용이 가능하고 사용시에는 import로 사용하는 방식을 말한다.
  • export 키워드를 통해 외부에서도 사용하기 원하는 것과 아닌 것을 구분해서 사용할 수 있다.
  • 이렇게 export 키워드를 이용해서 외부로 가져와 로컬 변수와 이름이 겹칠 시에는 에러가 발생한다.

 

Dymanic Import

동적으로 로딩하여 페이지 로딩 타임을 줄이거나 기능이 필요할 시에만 로드하고 파싱하고 컴파일하여 코드를 사용할 수 있게 해준다.

const button = document.getElementById("btn");

button.addEventListener("click", () => {
  import("./math.js").then((module) => {
    console.log("Add: ", module.add(1, 2));
    console.log("Multiply: ", module.multiply(3, 2));

    const button = document.getElementById("btn");
    button.innerHTML = "Check the console";
  });
});
  • 버튼이 클릭되었을 때 import해오기

 

 

Factory 패턴

클래스를 통해서 객체를 만들어내는 것이 아닌 함수를 호출하는 것으로 객체를 만들어 내는 방법으로 new 키워드를 사용하는 대신 함수호출의 결과로 객체를 만들 때 사용하는 패턴
  • 생각해보면 나는 항상 이러한 factory 패턴을 통해서 객체를 주로 만들었다. class를 사용하진 않은 듯..
const createObjectFromArray = ([key, value]) => ({
  [key]: value,
})

createObjectFromArray(['name', 'John']) // { name: "John" }
  • 이렇게 객체를 return하는 함수를 통해서 쉽게 객체를 만들 수 있다.

 

장점

  • 동일한 프로퍼티를 가진 여러 작은 객체를 만들 때 유용하며 원하는 객체를 쉽게 만들 수 있다.

단점

  • 자바스크립트에서 팩토리 함수는 new 키워드 없이 객체를 만드는 것에서 크게 벗어나지 않고, 화살표 함수를 이용하면 간결하게 작은 팩토리 함수를 만들 수 있다.
  • 클래스를 활용하면 메모리 절약에 좀 더 효과적이다.

 

 

Singleton 패턴 ⁉️

앱 전체에서 공유 및 사용되는 단일 인스턴스
  • 결론부터 말하자면 Singleton은 안티패턴 혹은 자바스크립트에서 하지 말아야 하는 것으로 언급된다
  • 자바스크립트에서는 클래스를 작성하지 않아도 객체를 만들 수 있기에 오버 엔지니어링이라 볼 수 있고 객체 리터럴을 사용해서도 동일한 구현을 할 수 있다.
  • 전역상태 관리를 위해 사용되는 패턴이지만 React에서는 Context나 전역상태 관리 라이브러리를 사용하는 것이 훨씬 낫다.
📢  Couner를 통해서 쉽게 예시를 들 수 있다.
숫자를 올리거나 내릴 수 있는 기능을 구현하였다. 근데 이걸 버튼 A에도 적용해야하고 버튼 B에도 적용해야할 때 Singleton 패턴을 사용하지 않으면 각자 instance를 생성하고 관리하기에 버튼 A의 값이랑 버튼 B의 값이 공유되지 않고 각각 관리가 된다.
이러한 문제점을 막고 A를 눌러서 1이 된 count 가  B를 눌렀을 떄 2가 되게 만들 수 있는 그런 기능이다.

 

더보기
let instance
let counter = 0

class Counter {
  constructor() {
    if (instance) {
      throw new Error('You can only create one instance!')
    }
    instance = this
  }

  getInstance() {
    return this
  }

  getCount() {
    return counter
  }

  increment() {
    return ++counter
  }

  decrement() {
    return --counter
  }
}

const singletonCounter = Object.freeze(new Counter())
export default singletonCounter

class를 이용해서 구현을 할 때 이러한 모습을 띈다.

단점

  • 객체 리터럴을 사용하면 훨씬 간단하게 구현이 가능하다
  • Singleton 패턴으로 구현된 코드는 테스트하는 것이 까다롭다.
  • 명확하지 않은 의존으로 인해 공유 시에 직접 수정하게 될 수  있어 예외로 이어질 수 있다.

 

 

Container / Presentational 패턴 ⁉️

비즈니스 로직으로부터 뷰를 분리하여 관심사 분리를 강제하는 패턴이다.

 

Presentatioanl Components: 데이터가 어떻게 사용자에게 보여질 지에 대해서만 다루는 컴포넌트 즉, 데이터는 건드리지 않고 props를 통해서 데이터만 받는다.

Container Components: 어떤 데이터가 보여질 지에 대해 다루는 컴포넌트로 Presentational 컴포넌트에 데이터를 전달하는 것이 주요 기능인 컴포넌트로 Container 컴포넌트 자체는 화면에 아무것도 랜더링하지 않는다.

 

사용되지 않는 이유

  • 대게 React Hooks로 대체가 가능하기에 Container 컴포넌트 없이도 stateless 컴포넌트를 쉽게 만들 수 있다. (커스텀 훅으로 비즈니스 로직을 뺴버림)

그럼에도 알면 좋은 점

  • 코드베이스에 대한 이해가 깊지 않은 개발자더라도 쉽게 수정이 가능하다.
  • 테스트하기 쉽다는 특징이 있다.

 

 

Command 패턴 ⁉️

특정 작업을 실행하는 개체와 메서드를 호출하는 개체를 분리할 수 있어 결합도를 낮출 수 있는 패턴

 

class OrderManager() {
  constructor() {
    this.orders = []
  }

  placeOrder(order, id) {
    this.orders.push(id)
    return `You have successfully ordered ${order} (${id})`;
  }

  trackOrder(id) {
    return `Your order ${id} will arrive in 20 minutes.`
  }

  cancelOrder(id) {
    this.orders = this.orders.filter(order => order.id !== id)
    return `You have canceled your order ${id}`
  }
}
const manager = new OrderManager()

manager.placeOrder('Pad Thai', '1234')
manager.trackOrder('1234')
manager.cancelOrder('1234')
  • 위와 같은 코드를 구현했을 떄에 OrderManager에 메서드가 종속적으로 구성되어 있다 이런 경우 placeOrder라는 메서드의 이름을 addOrder라고 바꾸게 되면 사용하는 호출하는 모든 곳에서 문제가 생기게 된다.
  • 이렇듯 메서드와 객체의 결합도를 낮추려할 때 종종 사용되는 패턴으로 많이 사용되진 않는다..
더보기
class OrderManager {
  constructor() {
    this.orders = [];
  }

  execute(command, ...args) {
    return command.execute(this.orders, ...args);
  }
}

class Command {
  constructor(execute) {
    this.execute = execute;
  }
}

function PlaceOrderCommand(order, id) {
  return new Command(orders => {
    orders.push(id);
    console.log(`You have successfully ordered ${order} (${id})`);
  });
}

function CancelOrderCommand(id) {
  return new Command(orders => {
    orders = orders.filter(order => order.id !== id);
    console.log(`You have canceled your order ${id}`);
  });
}

function TrackOrderCommand(id) {
  return new Command(() =>
    console.log(`Your order ${id} will arrive in 20 minutes.`)
  );
}

const manager = new OrderManager();

manager.execute(new PlaceOrderCommand("Pad Thai", "1234"));
manager.execute(new TrackOrderCommand("1234"));
manager.execute(new CancelOrderCommand("1234"));
  • 이렇게 사용하게 되면 앞서 말한 문제점을 해결하기 쉬워진다

 

 

 

Mixin 패턴 ⁉️

상속 없이 어떤 객체나 클래스에 재사용 가능한 기능을 추가할 수 있는 객체로 단독으로 사용할 순  없고 추가하는 목적으로 사용된다.
  • 컴포넌트의 믹스인은 복잡도를 증가시키고 재사용을 어렵게 만들기에
  • 현재 React 개발팀에서는 고차 컴포넌트의 사용을 강조하면 mixin을 사용하지 말라고 권장하고 있기에 사용하지 않는 것이 좋을 듯 하다.

 

 

Mediator / Middleware 패턴 ⁉️

컴포넌트들이 중재자 역할을 하는 객체를 통하여 직접 통신하지 않고 중재자 역할을 하는 객체릁 통해 통신하는 방식
  • 통신의 횟수가 많아지면 점점 흐름을 파악하는 것이 어렵다는 단점이 있다.

 

 

Render Props 패턴 ⁉️

JSX 엘리먼트를 props를 통해 컴포넌트에게 전달해 컴포넌트를 재사용할 수 있게 하는 또 다른 패턴이다
<Title render={() => <h1>I am a render prop!</h1>} />

const Title = props => props.render()
  • react hooks로 대체되었기에 대부분의 상황에서 hooks를 이용해서 사용을 한다.
  • 자식요소로도 충분히 전달 가능한 방법이에 많이 사용하지 않는다.
  • render prop 내에서는 생명주기 함수를 사용할 수 없다.

 

 

Flyweight 패턴 ⁉️

비슷한 객체를 대량으로 만들어야 할 경우 이미 존재하는 인스턴스를 재사용하여 메모리를 절약할 수 있게 해주는 패턴
  • 프로토타입 상속을 통해 비슷한 효과를 낼 수 있어 그리 중요하지 않게 되었다.

📌 reference

'CS > 프로그래밍' 카테고리의 다른 글

SOLID 원칙  (2) 2024.03.16
디자인 패턴 - 싱글톤 패턴  (3) 2024.02.28
솔리드 원칙  (1) 2024.01.20
고차 컴포넌트 (HOC, High Order Component)  (0) 2024.01.20
CSS vs CSS-in-CSS vs CSS-in-JS  (1) 2024.01.13