선언형 프로그래밍의 중요성

김영준·2023년 6월 15일
0

TIL

목록 보기
6/91
post-thumbnail

이번 글에서는 명령형 프로그래밍과 선언형 프로그래밍의 차이를 알아보고,
왜 선언적으로 프로그래밍을 작성해야 하는지 알아보도록 하자!


명령형 프로그래밍

"어떻게" 처리하는지에 대한 묘사
일일이 다 서술해야 한다.

function double(arr) {
  let results = [];
  for (let i = 0; i < arr.length; i++) {
    if (typeof arr[i] === "number") results.push(arr[i] * 2);
  }
  return results;
}

double([1, 2, 3, null, "a", undefined, 4]);

선언형 프로그래밍

"무엇을" 원하는지에 대한 묘사
무엇을 할 것이냐가 중요

function double(arr) {
  return arr.filter((value) => typeof value === "number").map((n) => n * 2); // number 형태의 값만 x2를 할 것이다!
}

double([1, 2, 3, null, "a", undefined, 4]);

명령형 프로그래밍에 비해 간결해서 가독성이 좋고 확장하기도 좋은 형태가 된 걸 볼 수 있다.


다른 예제를 살펴보자.

명령형 방식 코드

const data = [
  {
    name: "초코",
    colors: ["yellow", "white"],
    age: 7,
    ear: "unfolded",
  },
  {
    name: "레이",
    colors: ["white", "black", "brown"],
    age: 3,
    ear: "unfolded",
  },
  // ...
];

// 털색이 까만색이 포함되어 있으면서
// 귀가 접혀있지 않은 고양이들을 뽑기

function filterCats(cats) {
  let results = [];
  for (let i = 0; i < cats.length; i++) {
    const cat = cats[i];
    if (cat && cat.colors.includes("white") && cat.ear === "unfolded") {
      results.push(cat.name);
    }
  }
  return results;
}

선언형 방식 코드

// 털색이 까만색이 포함되어 있으면서
// 귀가 접혀있지 않은 고양이들을 뽑기

function filterCats(cats) {
  return cats
    .filter((cat) => cat &&
    cat.colors.includes("black") && cat.ear === "unfolded")
    .map((cat) => cat.name);
}

확실히 선언형 방식이 코드도 간결하고 가독성도 좋다.
또한 추가적인 기능을 넣을 때도 쉽게 작성할 수 있다.

그럼 프론트엔드는 어떤 식으로 코드를 작성해야 할까?

UI에서도 선언형 방식으로 코드를 작성하는 것은 매우 중요하다.

먼저 명령형 방식을 살펴보자.

// 버튼을 3개 만든다.
const $button1 = document.createElement("button");
$button1.textContent = "Button1";

const $button2 = document.createElement("button");
$button2.textContent = "Button2";

const $button3 = document.createElement("button");
$button3.textContent = "Button3";

const toggleButton = ($button) => {
  if ($button.style.textDecoration === "line-through") {
    $button.style.textDecoration = "none";
  } else {
    $button.style.textDecoration = "line-through";
  }
};

// 만든 버튼을 화면에 그린다.
const $main = document.querySelector("body");
$main.appendChild($button1);
$main.appendChild($button2);
$main.appendChild($button3);

// 버튼을 클릭하면 삭선이 그어진다.
document.querySelectorAll("button").forEach(($button) => {
  $button.addEventListener("click", (e) => {
    const { target } = e;
    toggleButton(target);
  });
});

버튼을 생성할 때마다 복잡한 코드가 여러 번 반복된다.
명령형이기 때문에 순서에 의존해서 코드가 꼬일 수도 있고 가독성도 좋지 않다.

이제 선언적으로 코드를 변경하고 차이점을 알아보자.
먼저 위 코드를 컴포넌트 방식으로 추상화할 것이다.

// 토글 버튼을 컴포넌트 방식으로 추상화하기

function ToggleButton({ $target, text }) {
  const $button = document.createElement("button");
  $target.appendChild($button);

  // render 함수를 정의
  this.render = () => {
    $button.textContent = text;
  };

  $button.addEventListener("click", () => {
    if ($button.style.textDecoration === "line-through") {
      $button.style.textDecoration = "";
    } else {
      $button.style.textDecoration = "line-through";
    }
  });

  this.render();
}

const $main = document.querySelector("body");

new ToggleButton({ $target: $main, text: "Button1" });

new ToggleButton({ $target: $main, text: "Button2" });

new ToggleButton({ $target: $main, text: "Button3" });

위 코드를 살펴보면 새로운 버튼을 생성할 때는 new 키워드로 ToggleButton 객체만 생성해 주면 된다.
또한 버튼에 대한 새로운 기능 추가가 쉬워지고 렌더링 되는 시점 등을 명확하게 알 수 있다.

Button1에 3번 클릭할 때마다 alert를 띄우는 기능을 넣어보자.

//3번 클릭할 때 마다 alert 띄우기
function ToggleButton({ $target, text, onClick }) {
  const $button = document.createElement("button");
  let clickCount = 0;

  $target.appendChild($button);

  // render 함수를 정의
  this.render = () => {
    $button.textContent = text;
  };

  $button.addEventListener("click", () => {
    clickCount++;
    if ($button.style.textDecoration === "line-through") {
      $button.style.textDecoration = "";
    } else {
      $button.style.textDecoration = "line-through";
    }
    if (onClick) {
      onClick(clickCount); // onClick이 발생하면 clickCount를 인자로 넘겨줌
    }
  });

  this.render();
}

const $main = document.querySelector("body");

new ToggleButton({
  $target: $main,
  text: "Button1",
  onClick: (clickCount) => {
    if (clickCount % 3 === 0) {
      alert("3번째 클릭");
    }
  }, // BUtton1에만 alert를 띄우는 방법
});

new ToggleButton({ $target: $main, text: "Button2" });

new ToggleButton({ $target: $main, text: "Button3" });

명령형 방식으로 프로그래밍을 하면 각각 다른 className을 지정해 주고 이벤트를 발생시키는 코드도 일일이 작성해야 한다.

반대로 선언형 방식으로는 위 코드처럼 별도의 className을 지정해 주지 않아도 되고 각 버튼의 인자를 통해 onClick 이벤트를 지정할 수 있다.

하지만 코드를 살펴보면 DOM을 직접적으로 접근해서 style을 변경하고 있다.

이러한 방식보다는 상태를 추상화하고, 상태에 따라서 style이 변경될 수 있게 코드를 작성하는 것이 더 좋은 방식이다.

아래 방식으로 작성하면 DOM에 직접 접근하지 않으면서 style을 변경하고 코드의 복잡도를 낮출 수 있다.

// 상태를 추상화하고 상태에 따라 style을 변경

function ToggleButton({ $target, text, onClick }) {
  const $button = document.createElement("button");
  $target.appendChild($button);

  this.state = {
    clickCount: 0,
    toggled: false,
  };

  this.setState = (nextState) => {
    this.state = nextState;
    this.render();
  };

  // render 함수를 정의
  this.render = () => {
    $button.textContent = text;
    //컴포넌트의 상태에 따라 동작하도록 변경
    $button.style.textDecoration = this.state.toggled ? "line-through" : "none";
  };

  $button.addEventListener("click", () => {
    this.setState({
      // 클릭하면 상태를 업데이트
      clickCount: this.state.clickCount + 1,
      toggled: !this.state.toggled,
    });

    if (onClick) {
      onClick(this.state.clickCount); // onClick이 발생하면 clickCount를 인자로 넘겨줌
    }
  });

  this.render();
}

const $main = document.querySelector("body");

new ToggleButton({
  $target: $main,
  text: "Button1",
  onClick: (clickCount) => {
    if (clickCount % 3 === 0) {
      alert("3번째 클릭");
    }
  }, // BUtton1에만 alert를 띄우는 방법
});

new ToggleButton({ $target: $main, text: "Button2" });

new ToggleButton({ $target: $main, text: "Button3" });

n초 뒤에 자동으로 토글 되는 버튼 만들기

// n초 뒤에 자동으로 토글 되는 버튼 만들기
function TimerButton({ $target, text, timer = 3000 }) {
  const button = new ToggleButton({
    $target,
    text,
    onClick: () => {
      setTimeout(() => {
        button.setState({
          ...button.state,
          toggled: !button.state.toggled,
        });
      }, timer);
    },
  });
}

function ToggleButton({ $target, text, onClick }) {
  const $button = document.createElement("button");
  $target.appendChild($button);

  this.state = {
    clickCount: 0,
    toggled: false,
  };

  this.setState = (nextState) => {
    this.state = nextState;
    this.render();
  };

  // render 함수를 정의
  this.render = () => {
    $button.textContent = text;
    //컴포넌트의 상태에 따라 동작하도록 변경
    $button.style.textDecoration = this.state.toggled ? "line-through" : "none";
  };

  $button.addEventListener("click", () => {
    this.setState({
      // 클릭하면 상태를 업데이트
      clickCount: this.state.clickCount + 1,
      toggled: !this.state.toggled,
    });

    if (onClick) {
      onClick(this.state.clickCount); // onClick이 발생하면 clickCount를 인자로 넘겨줌
    }
  });

  this.render();
}

const $main = document.querySelector("body");

new ToggleButton({
  $target: $main,
  text: "Button1",
  onClick: (clickCount) => {
    if (clickCount % 3 === 0) {
      alert("3번째 클릭");
    }
  }, // BUtton1에만 alert를 띄우는 방법
});

new ToggleButton({ $target: $main, text: "Button2" });

new ToggleButton({ $target: $main, text: "Button3" });

new TimerButton({
  $target: $main,
  text: "3초 뒤 자동으로 변경",
});
new TimerButton({
  $target: $main,
  text: "10초 뒤 자동으로 변경",
  timer: 1000 * 10,
});

버튼을 그룹으로 만들기

function TimerButton({ $target, text, timer = 3000 }) {
  const button = new ToggleButton({
    $target,
    text,
    onClick: () => {
      setTimeout(() => {
        button.setState({
          ...button.state,
          toggled: !button.state.toggled,
        });
      }, timer);
    },
  });
}

function ToggleButton({ $target, text, onClick }) {
  const $button = document.createElement("button");
  $target.appendChild($button);

  this.state = {
    clickCount: 0,
    toggled: false,
  };

  this.setState = (nextState) => {
    this.state = nextState;
    this.render();
  };

  // render 함수를 정의
  this.render = () => {
    $button.textContent = text;
    //컴포넌트의 상태에 따라 동작하도록 변경
    $button.style.textDecoration = this.state.toggled ? "line-through" : "none";
  };

  $button.addEventListener("click", () => {
    this.setState({
      // 클릭하면 상태를 업데이트
      clickCount: this.state.clickCount + 1,
      toggled: !this.state.toggled,
    });

    if (onClick) {
      onClick(this.state.clickCount); // onClick이 발생하면 clickCount를 인자로 넘겨줌
    }
  });

  this.render();
}

function ButtonGroup({ $target, buttons }) {
  const $group = document.createElement("div");
  let isInit = false; // render 함수가 여러 번 호출될 수 있으므로 플래그를 지정

  this.render = () => {
    if (!isInit) {
      buttons.forEach(({ type, ...props }) => {
        if (type === "toggle") {
          new ToggleButton({ $target: $group, ...props });
        } else if (type === "timer") {
          new TimerButton({ $target: $group, ...props });
        }
      });

      $target.appendChild($group);
      isInit = true;
    }
  };

  this.render();
}

const $main = document.querySelector("body");

new ButtonGroup({
  $target: $main,
  buttons: [
    {
      type: "toggle",
      text: "토글 버튼",
    },
    {
      type: "toggle",
      text: "토글 버튼",
    },
    {
      type: "timer",
      text: "타이머",
      timer: 1000,
    },
  ],
});

인자로 전달된 값들에 의해서 동작하기 때문에 외부에서 접근할 수 없다.
따라서 독립적으로 동작할 수 있으면서 상태를 기반으로 추상화되어있는 UI를 작성할 수 있다.

결론

프론트엔드 개발자는 선언적으로 프로그래밍을 하는 것이 중요하다!
선언형 프로그래밍의 장점은

  • 재사용성이 좋아진다.
  • 가독성이 좋다.
  • 코드의 복잡성이 완화된다.
  • 상태를 추상화하여 DOM에 직접적으로 접근하지 않아서 안전하다.

궁극적으로 이걸 잘하면 리액트든 뷰든 다 잘하게 되어있다!

profile
꾸준히 성장하는 개발자 블로그

0개의 댓글