(번역) 자바스크립트에서의 옵서버 패턴 - 반응형 동작의 핵심

sehyun hwang·2022년 2월 16일
17

FE 번역글

목록 보기
3/36

원문 : https://blog.bitsrc.io/the-observer-pattern-in-javascript-the-key-to-a-reactive-behavior-f28236e50e10

많은 신규 개발자들은 React와 같은 프레임워크를 마법과 같이 생각하는 경향이 있습니다. 왜냐하면 그들이 개발자가 되기까지 배웠던 모든 것들과 실제 데이터 흐름이 진행되는 방식이 너무 다르게 느껴지기 때문입니다.

물론, 내부적으로 어떤 일이 발생하는지 모른다면 마법처럼 느껴질 수 있습니다. 하지만 Arthur C. Clarke는 이렇게 말합니다.

“충분히 진보한 기술은 마법과 다름없습니다.”

따라서 지금부터 당신이 실제로 마주했던 반응형 동작에 대한 기본적인 원칙과 (1) 왜 그것이 마법이 아닌지 (2) 왜 충분히 이해할만한 가치를 지녔는지 알아보도록 합시다.

참고로 이 글에서는 React를 단일 패턴으로 너무 단순화하여 전체 프레임워크가 어떻게 작동하는지 이해할 수 있도록 설명하려는 것은 아닙니다. 저는 그저 여러 사용 사례에 걸쳐 사용할 수 있는 단일 원칙에 대해 말씀드리고자 하는 것이고, 그 사례 중 하나가 프런트엔드 프레임워크입니다.

옵서버 패턴에 대하여

시작하기 앞서서 가장 먼저 알아야 할 것은 옵서버 패턴 자체 입니다.

그리고 장담컨데, 옵서버 패턴에 대해 이해하고 나면 당신은 더 이상 React와 같은 프레임워크가 마법처럼 느껴지지 않을 것 입니다. (기대를 깨뜨렸다면 미안합니다.)

옵서버 패턴은 객체의 동작 방식과 어떤 일이 발생했을 때 객체 간에 서로 어떻게 상호작용하는가를 다룬다는 점에서 “behavioral pattern” 으로 잘 알려져 있습니다.

이 말인즉슨 옵서버 패턴은 객체 집합(관찰자, observers)이 다른 단일 객체(관찰 대상, observed)의 상태 변화에 관심이 있을때 “관찰자 - 관찰 대상” 유형의 관계를 구조화하는 방법을 나타냅니다.

여기에서 핵심은 “관찰자"들이 항시 관찰 대상을 주시하고 있는 게 아니라는 것입니다. 대신에 그들은 변경 사항에 대한 알림을 받기 위해 구독(subscribe)하며, 관찰 대상에게 변화가 생겼을 때 알림을 받습니다.

이 미세한 차이점은 중요합니다. 왜냐하면 관찰 대상을 항시 주시하고 있다는 것은 어떠한 변경 사항을 확인하기 위해서 계산 자원과 시간을 소비한다는 뜻이기 때문입니다. 단 하나의 대상만을 관찰할 때는 위와 같은 사항이 큰 문제점이 아닐 수 있지만, 만약 당신이 프로그램 내의 관찰자를 수백 개(더 많게는 수천 개) 까지 늘리려는 상황에서는 매우 많은 시간과 자원을 소비해야하는 큰 문제가 발생할 수 있습니다.

만약 상태 변화를 기다리는 동안 관찰자들이 하던 일을 계속 진행할 수 있다면, 또는 자원을 소비하지 않고 유휴 상태(IDLE)로 머무를 수 있다면 확연한 성능 차이를 가져올 것입니다.

설명이 아직도 와닿지 않는다면 신문을 보기 위해 매일 사러 나갔다 오는 것과 신문 구독을 함으로써 매일 문 앞으로 신문이 배달되는 상황을 생각해보세요. 전자의 상황이 분명히 더 큰 에너지를 소비하게 될 것입니다.

UML

위의 다이어그램은 옵서버 패턴에 대한 UML이 아닌, 3개의 관찰자가 관찰 대상과 어떻게 상호 작용하는지를 나타낸 단순 예제입니다. 간단합니다. 외부에서 각 관찰자가 어떻게 addSubscriber 메서드를 호출하는지와 어떻게 notifySubscribers 메서드가 특정 event 매개변수와 함께 각 관찰자의 update 메서드를 호출하는지를 알 수 있습니다. 여기서 event 의 경우 무엇이 방금 변경되었는지에 대한 세부 사항을 나타냅니다. 관찰자가 관찰 대상의 상태에 직접 접근하도록 구현할 수도 있지만, 위의 방법이 관찰자에게 무엇이 변경되었는지를 구체적으로 알려주기 때문에 훨씬 쉬운 방법입니다.

이제는 옵서버 패턴뒤에 실제로 마법이 있지 않다는 것을 알 수 있을 겁니다. 단지, 관찰자의 시선(어쩌면 개발자의 시선)에서는 너무 멋들어지게 동작하기에 마법처럼 보이는 것입니다.

자바스크립트로 옵서버 패턴 구현하기

지금부터는 코드를 통해 좀 더 알아보겠습니다.

이번 예제에서는 변수의 값을 순환하는 loop이 있고, 순환하다가 특정 기준을 만족하는 값을 발견했을 때 우리가 원하는 동작을 실행하고 싶다고 가정해보겠습니다.

예를 들어, 1부터 1000까지 순환하는 loop이 있고 우리는 홀수일 때마다 반응하고 싶다고 합시다. 제가 지금까지 말한 것을 도식화해보면 아래와 같습니다.

UML2

여기에서는 자바스크립트를 다루고 있으므로 기억해야 할 점이 있습니다. 적어도 지금까지는 자바스크립트에는 private 메서드나 속성이 없으며 추상 클래스와 메서드 또한 없습니다. 그러므로 필요시에는 상황에 맞춰서 임기응변식으로 구현해야 합니다.

참고 : ES2022에 Private class fields가 추가되었습니다.
https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes/Private_class_fields

우리는 아래와 같이 동작하도록 구현할 수 있습니다.

const Looper = require("./looper")
const OddNotifier = require("./oddNotifier")

const l = new Looper(1, 100000)
l.addObserver(new OddNotifier())
l.run();

매우 간단한 구현이지만 요점은 알 수 있습니다. Looper 클래스는 loop의 시작과 끝을 순환하며 run 메서드를 통해 실행합니다. 그리고 실행하기 전에 addObserver 메서드를 통해 우리가 원하는 만큼의 관찰자를 추가할 수 있습니다. 관찰자가 ‘언제' 그리고 ‘어떻게' 반응하는지를 이해하는 데 필요한 로직은 각 관찰자(이 예제의 경우 OddNotifier) 안에 캡슐화되어 있습니다.

위의 로직을 실행하면 아래와 같은 결과값이 출력됩니다.

result1

보다시피 매우 성가신 관찰자를 구현했습니다.

로직을 좀 더 자세히 살펴보겠습니다.

const Observer = require("./observer")

class OddNotifier extends Observer {

    constructor() {
        super()
    }

    eventIsRelevant(evnt) {
        return (evnt.evntName == "new-index" && evnt.value % 2 != 0)
    }

    reactToEvent(evnt) {
        console.log("----------------------")
        console.log("Odd number found!")
        console.log(evnt.value)
        console.log("----------------------")
    }
}

보다시피 (앞선 UML diagram에 따르면) Observer 부모 클래스 내에 정의되어있던 eventIsRelevantreactToEvent 메서드를 재정의했습니다. 여기에서는 확실히 event의 이름과 값을 확인하고 있습니다. 확인된 값이 우리의 기준을 만족한다면 메서드가 true 를 반환합니다. update 메서드는 재정의하지 않은 것에 주목해봅시다. 부모 클래스내에 있는 update 메서드의 기본 구현 자체가 이미 충분하기 때문에 별도로 재정의 할 필요가 없습니다. 적절한 상황에서 부모 클래스가 reactToEvent 메서드를 호출할 것입니다. 이제 그 내용을 간단히 살펴봅시다.

class Observer {

    update(event) {
        if(this.eventIsRelevant(event)) {
            this.reactToEvent(event)
        }
    }

    eventIsRelevant() {
        throw new Error("This needs to be implemented")
    }

    reactToEvent() {
        throw new Error("This needs to be implemented")
    }
}

module.exports = Observer;

화려한 코드는 아니지만, 이벤트를 받고 발생한 이벤트와 관련이 있다면 이에 반응하도록 매우 간단한 update 메서드를 구현했습니다. 이미 앞선 예제에서 봤듯이 그것이 의미하는 바가 무엇인지를 정의하는 것은 각 구체적인 관찰자의 담당입니다.

우리의 관심 주체인 Looper 클래스는 매우 복잡한 로직을 따라 값을 순환하는 작업을 처리하고 새로운 이벤트가 발동되어야 할 때마다 관찰자들에게 이를 알립니다. 코드를 같이 살펴보죠.

const Subject = require("./subject")

module.exports = class Looper extends Subject {

    constructor(first, last) {
        super()
        this.start = first;
        this.state = first;
        this.end = last;
    }

    run() {
        for(this.state = this.start; this.state < this.end; this.state++) {
            this.notifyObservers({
                evntName: "new-index",
                value: this.state 
            })
        }
    }
}

위 코드에서는 어떻게 새로운 관찰자를 추가하고, 그들에게 알리는 것(notify)이 무슨 의미인지에 대해서는 고려하고 있지 않습니다. notifyObservers 라는 메서드가 있고 모든 관련 이벤트에서 해당 메서드가 호출되어야 한다는 것만 알면 됩니다. 이 예제의 경우는 “루프 내의 매 새로운 값 마다" 로 해석할 수 있겠죠. 그러나 이 외에 어떤 상황도 가능합니다. 사실 로직이 더 복잡했다면 더 많은 이벤트를 발동시키는 것이 가능하고 각각의 관찰자 내부의 구체적인 로직에 따라, 발생한 이벤트가 그들 자신과 관련이 있는지 없는지를 결정할 수 있을 것입니다.

마지막으로, Subject 클래스는 관찰자들을 모으고 그들에게 알리는 것만 고려하면 되기 때문에 매우 간단합니다.

class Subject {

    constructor() {
        this.observers = [];

    }

    addObserver(obs) {
        this.observers.push(obs)
    }

    notifyObservers(event) {
        this.observers.forEach( o => o.update(event))
    }
}

module.exports = Subject;

이제 조금 더 나아가서 실제 상황과 좀 더 밀접한 관련이 있는 예(특히 React 개발자인 경우)를 보여드리겠습니다.

let [looper, increaseLooper] = useState(1)

console.log("Initial state: ", looper.state)

console.log("Increasing the value by 1")
increaseLooper()
console.log("Increasing the value by 1")
increaseLooper()
console.log("Increasing the value by 1")
increaseLooper()

저는 변수 looper의 초기 상태를 1로 설정하고 이를 매번 1씩 증가시키는 함수를 반환하는 hook을 만들었습니다. 직접 looper 의 내부 상태를 1씩 증가시키는 우리의 능력 대신에, 현재의 코드가 "마법"이 작동하는데 필요한 모든 것을 갖췄다고 생각하시나요?

왜냐하면 현재의 코드는 아래와 같이 동작하기 때문입니다.

result2

아래는 hook의 구현 방법입니다.

function useState(start) {
    let l = new Looper(start, start * 10000)

    l.addObserver(new OddNotifier())

    let fn = () => {
        l.increase()
    }

    return [l, fn]
}

이제 옵서버 패턴을 이해했으니 이상하지 않죠?

그리고 새로운 increase 메서드는 다음과 같은 방식에 지나지 않습니다.

//....
    increase() {
        this.state++;
        this.notifyObservers({
            evntName: "new-index",
            value: this.state 
        })
    }
//...

내부 상태를 증가시키고, 모든 관찰자들에게 이를 알리는 것이 전부입니다.


이제 옵서버 패턴의 베일이 벗겨지고 hook의 비밀스러운 동작이 미리 정해진 관찰자 집합에 지나지 않는다는 것이 밝혀졌습니다. 어떠신가요?

이전에 옵서버 패턴을 사용해 본 적이 있나요? Node.js의 eventEmitter와 함께 사용해보셨나요? eventEmitter는 우리를 위한 편리한 기능을 제공하기 때문에 함께 사용한다면 옵서버 패턴을 훨씬 쉽게 구현할 수가 있습니다.

여러분이 가장 좋아하는 디자인 패턴이나, 좀 더 알아보고 싶은 패턴에 대해서 공유해주세요. 그러면 제가 다음번에 함께 다뤄보도록 하겠습니다!

0개의 댓글