이번 글에서는 명령형 프로그래밍과 선언형 프로그래밍의 차이를 알아보고,
왜 선언적으로 프로그래밍을 작성해야 하는지 알아보도록 하자!
"어떻게" 처리하는지에 대한 묘사
일일이 다 서술해야 한다.
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);
}
확실히 선언형 방식이 코드도 간결하고 가독성도 좋다.
또한 추가적인 기능을 넣을 때도 쉽게 작성할 수 있다.
먼저 명령형 방식을 살펴보자.
// 버튼을 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 객체만 생성해 주면 된다.
또한 버튼에 대한 새로운 기능 추가가 쉬워지고 렌더링 되는 시점 등을 명확하게 알 수 있다.
//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초 뒤에 자동으로 토글 되는 버튼 만들기
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를 작성할 수 있다.
프론트엔드 개발자는 선언적으로 프로그래밍을 하는 것이 중요하다!
선언형 프로그래밍의 장점은
궁극적으로 이걸 잘하면 리액트든 뷰든 다 잘하게 되어있다!