🔗 이 포스트에 대해 더 궁금하신가요? 다음 주소를 참고해주세요!
🗒️ 이 글의 수정 내역 (마지막 수정 일자: 없음)
지난 주는 강아지가 내내 아파서 개발하기가 곤란했어요. 🥲
늦었지만, 부랴부랴 움직임 구현에 대한 글을 써보고자 합니다.
지난 글까지 잘 살펴보셨다면, 이제 animation을 캔버스에서 동작시키는 로직까지 정상적으로 동작할 거에요!
그렇다면, 이 animation이 어떤 것을 연속적으로 보여주는 건지 생각해봅시다.
우리가 이 애니메이션을 동작시킴으로써 달성하려 하는 것은 무엇일까요? 바로 메타볼이 움직이는 거겠죠?
오늘은 이 움직임을 살펴보고자 합니다.
자. 우리가 Metaball
에서 x, y, r
등을 다양하게 설정했는데요.
여기서 메타볼의 캔버스 내에서의 position을 담당하고 있는 것은 바로 x
, y
입니다.
그러니 매우 간단해요. 그냥 x
값과 y
값을 변화시켜주면 돼요.
이 과정을 담당하는 메서드를 move
라 하겠습니다.
export class DynamicMetaball implements Metaball {
// ...
move() {
this.setX(this.x + this.vx);
this.setY(this.y + this.vy);
this.draw();
}
}
그리고 우리가 값을 변경했으니, 이에 맞는 결과값을 캔버스에 그려내주어야 합니다.
이것을 draw
라고 하겠습니다.
export class DynamicMetaball implements Metaball {
// ...
draw() {
this.ctx.save();
this.ctx.beginPath();
this.ctx.fillStyle = '#f7f711';
this.ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2);
this.ctx.fill();
this.ctx.closePath();
this.ctx.restore();
}
}
그런데 말이죠. 이것이 과연 최선일까요?
지금처럼 하게 된다면 어떤 문제가 발생할 수 있을지 다음 리스트를 보며 고민해봅시다.
DynamicMetaball
의 move
는 항상 똑같이 정의될까DynamicMetaball
의 draw
는 항상 똑같이 정의될까저는 이번에 이전 로직들의 퍼포먼스를 비교하는 과정에 있어 이러한 문제들을 유념해야 한다는 것을 여실히 깨달았어요. 그리고 현재의 메서드들을 그대로 사용한다면, 현재의 Metaball
을 그대로 사용하기는 곤란하죠.
그렇다면 여기서 파생되는 문제는 어떤 게 있을까요? 👀
DynamicMetaball
을 그대로 확장해버리면 draw
move
메서드에 대해 리스코프 치환의 원칙을 위배할 가능성이 높아요.DynamicMetaball
의 움직임을 그렇다고 다시 또 정의한다는 것은 개방-폐쇄 원칙에 위배되죠.즉, 확장에 있어서도, 재정의에 있어서도 유연하지가 않아요. 이는 좋은 설계라 보기 힘들었어요.
따라서 우리는 이러한 문제들을 해결할 방법이 필요하겠군요! 🙇🏻♂️
사실 이러한 문제를 해결할 수 있는 방안들이 몇 개가 있습니다.
데코레이터 패턴을 사용할 수도 있을 것 같고, 전략 패턴 등 다양한데요.
그 중 저는 전략패턴을 사용해보려 합니다.
전략패턴을 사용하려는 이유는 다음과 같아요.
이제 전략을 만들어볼까요?
abstract class Strategy {
abstract exec(...args: unknown[]): void;
abstract before?: (...args: unknown[]) => void;
abstract after?: (...args: unknown[]) => void;
}
export class MoveStrategy implements Strategy {
before?: (...args: unknown[]) => void;
after?: (...args: unknown[]) => void;
constructor() {}
setBefore(callback: (...args: unknown[]) => void) {
this.before = callback.bind(this);
}
setAfter(callback: (...args: unknown[]) => void) {
this.after = callback.bind(this);
}
exec(metaball: DynamicMetaball) {
// console.log('this: ', metaball, this);
this.before?.();
const {
x,
y,
v: {x: vx, y: vy},
} = metaball;
metaball.setX(x + vx);
metaball.setY(y + vy);
metaball.draw();
this.after?.();
}
}
그렇다면, 이제 우리는 옵저버로부터 이를 적용해주면 되겠죠?
한 번 적용해봅시다.
export class AnimationSubject implements Subject {
// 이전 코드 중략...
public notifyUpdateMoveStrategy({
moveStrategy,
key,
}: IDynamicMetaballMoveStrategy): void {
this.observers.forEach(observer => {
if (observer.key === key) {
(observer as DynamicMetaballsObserver).updateMoveStrategy(moveStrategy);
observer.update();
}
});
}
}
export class DynamicMetaballsObserver implements MetaballsAnimationObserver {
constructor(public metaballs: DynamicMetaballs, public key: string) {}
updateMoveStrategy(moveStrategy: MoveStrategy) {
this.metaballs.setMoveStrategy(moveStrategy);
}
// 이전 코드...
}
이렇게 하면 우리는 Subject
를 통해 구독한 옵저버들에게 MoveStrategy
에 대한 것을 전달하여 해당 알고리즘을 시킬 수 있죠.
이때, 나머지들의 로직을 구체화하여 move
를 실제로 만들어봅시다!
export class MetaballAnimation implements CanvasAnimation {
// 이전 코드 생략...
setDynamicMetaballMove({moveStrategy, key}: IDynamicMetaballMoveStrategy) {
this.canvas.setDynamicMetaballMoveStrategy({moveStrategy, key});
}
// 이전 코드 생략...
}
const $target = document.body;
const app = new MetaballAnimation({
canvas: new MetaballCanvas({
gradients: ['#123141', '#235234'],
width: window.innerWidth,
height: window.innerHeight,
type: ECanvasGradientType.linear,
options: {
autoplay: true,
},
}),
dataset: {
static: [{x: 30, y: 100, r: 20}],
dynamic: [
{
x: 120,
y: 60,
r: 20,
v: {x: 0.1, y: 0.1},
vWeight: 1,
},
],
},
});
function main() {
const moveStrategy = new MoveStrategy();
app.setDynamicMetaballMove({
moveStrategy,
key: EMetaballObserverKeys.dynamic,
});
app.mount($target);
}
main();
export class MetaballCanvas implements GradientCanvas {
// 이전 코드 생략...
constructor({
type,
width,
height,
gradients,
options,
}: Omit<
GradientCanvas,
| '$canvas'
| 'ctx'
| 'render'
| 'mount'
| 'draw'
| 'getLinearGradient'
| 'getRadialGradient'
>) {
// 이전 코드 생략...
this.init();
}
init() {
this.$canvas.width = this.width;
this.$canvas.height = this.height;
}
setDynamicMetaballMoveStrategy({
moveStrategy,
key,
}: IDynamicMetaballMoveStrategy) {
this.metaballAnimationSubject.notifyUpdateMoveStrategy({
moveStrategy,
key,
});
}
draw(background: CanvasGradient) {
this.ctx.clearRect(0, 0, this.width, this.height);
this.ctx.fillStyle = background;
this.ctx.fillRect(0, 0, this.width, this.height);
// 기존의 컨텍스트 정보를 바뀌지 않도록 저장해줍시다.
this.ctx.save();
}
}
이렇게 하면 이제, 우리는 main
을 통해 호출하여 원하는 moveStrategy
를 바깥쪽에서 달아줄 수 있게 되었어요 😄
한 번 동작시켜볼까요?
잘 동작하는군요!
가장 힘든 주말을 보냈어요.
강아지가 아픈 바람에, 오늘까지 해야 할 개발도 제대로 하질 못했네요.
물론 지금도 전혀 낫지를 않아서 마음을 졸이고 있지만,
마음이 아픈 만큼 더 간절하게, 열심히 개발해야겠다는 생각을 갖게 되었어요.
이번에 꽤나 긴 글로 작성되었는데, 끝까지 읽어주셔서 감사드려요 🥰
전략 패턴은 생각보다 많은 곳에서 사용되는 패턴이니, 알아두면 정말 좋은 것 같아요.다음에는,
draw
로직에 대해 좀 더 재사용할 수 있게 할 수 있지 않을까 고민하며, 이를 리팩토링하는 시간을 갖도록 할게요. 이상!