최근회사에서 리액트 코드를 작성하다 보면 어떻게 하면 더 좋은 컴포넌트를 설계할 수 있을지 고민될 때가 많습니다.
특히 코드가 복잡해지고 유지보수가 어려워질수록, 더 나은 방법이 필요하다고 느끼게 됩니다.
이 글에서는 명령형(Imperative) 프로그래밍과 선언형(Declarative) 프로그래밍의 차이를 살펴보고,
어떤 방식이 리액트 컴포넌트 설계에 더 적합한지 고민해보려고 합니다.
프로그래밍을 할 때 코드를 작성하는 방식에는 크게 명령형(Imperative)과 선언형(Declarative) 두 가지 스타일이 있습니다.
각각의 차이를 이해하기 위해 간단한 배열 필터링 예제를 살펴보겠습니다.
명령형 프로그래밍에서는 어떻게(How) 할 것인지 단계별로 절차를 직접 명시해야 합니다.
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
evenNumbers.push(numbers[i]);
}
}
console.log(evenNumbers); // [2, 4]
for 루프를 사용하여 배열을 직접 순회하며 조건을 검사if문을 사용하여 짝수인지 확인 후 새로운 배열에 추가선언형 프로그래밍에서는 무엇(What)을 원하는지만 선언하면, 내부적인 처리는 추상화된 함수가 알아서 수행합니다.
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // [2, 4]
filter()를 사용하여 간결하게 필터링for 루프를 사용하지만, 직접 작성할 필요 없음명령형과 선언형의 차이는 DOM 조작에서도 확인할 수 있습니다.
Vanilla JavaScript와 React를 비교해보겠습니다.
명령형 방식에서는 DOM을 직접 조작하며, 각각의 단계별로 실행해야 합니다.
const button = document.createElement("button");
button.innerText = "Click Me";
button.style.backgroundColor = "blue";
button.style.color = "white";
document.body.appendChild(button);
button.addEventListener("click", function () {
alert("Button clicked!");
});
createElement()로 버튼을 생성하고 속성을 하나씩 추가해야 함React를 사용하면 UI를 선언적으로 기술할 수 있습니다.
function App() {
return (
<button style={{ backgroundColor: "blue", color: "white" }} onClick={() => alert("Button clicked!")}>
Click Me
</button>
);
}
리액트 컴포넌트 설계에서도 명령형과 선언형의 차이가 명확하게 드러납니다.
명령형 방식에서는 컴포넌트 내부에서 DOM을 직접 조작하는 방식으로 구현됩니다.
import { useEffect } from "react";
function App() {
useEffect(() => {
const button = document.createElement("button");
button.innerText = "Click Me";
button.style.backgroundColor = "blue";
button.style.color = "white";
document.body.appendChild(button);
button.addEventListener("click", () => alert("Button clicked!"));
return () => {
button.removeEventListener("click", () => alert("Button clicked!"));
document.body.removeChild(button);
};
}, []);
return <div>Check the button added to the DOM</div>;
}
export default App;
useEffect에서 DOM을 직접 조작해야 함cleanup 함수를 사용해 수동으로 정리해야 함React의 철학을 따르는 선언형 방식으로 리팩토링하면 코드가 훨씬 깔끔해집니다.
import { useState } from "react";
function App() {
const [isClicked, setIsClicked] = useState(false);
return (
<div>
<button
style={{ backgroundColor: "blue", color: "white" }}
onClick={() => setIsClicked(true)}
>
Click Me
</button>
{isClicked && <p>Button clicked!</p>}
</div>
);
}
export default App;
useState를 사용하여 React 방식으로 상태 관리리액트에서는 대부분 선언형 방식을 따르는 것이 좋지만, 특정 상황에서는 명령형 방식이 필요할 수 있습니다.
| 상황 | 선언형 가능? | 명령형 필요? | 해결 방법 |
|---|---|---|---|
| 포커스 설정 | ❌ 불가능 | ✅ 필요 | useRef + focus() |
| 스크롤 이동 | ❌ 불가능 | ✅ 필요 | useRef + scrollIntoView() |
| Canvas, WebGL | ❌ 불가능 | ✅ 필요 | useRef + getContext("2d") |
| 외부 라이브러리 | ❌ 불가능 | ✅ 필요 | useRef + useEffect |
즉, React에서는 "선언형을 기본으로, 명령형을 보조적으로" 사용하면 더 좋은 컴포넌트를 설계할 수 있습니다.
리액트에서 선언형 프로그래밍을 사용하는 것이 더 나은 컴포넌트 설계로 이어진다는 점을 확인할 수 있었습니다.
하지만 때때로 명령형 방식이 필요한 경우도 존재하며, useRef나 useEffect를 활용하여 직접 DOM을 조작해야 합니다.
| 방식 | 명령형(Imperative) | 선언형(Declarative) |
|---|---|---|
| 코드 스타일 | 절차 지향, 직접 명령 | 추상화된 함수 활용 |
| 유지보수성 | 복잡하고 어려움 | 가독성이 좋고 유지보수 용이 |
| 적합한 경우 | 포커스, 애니메이션, 외부 라이브러리 | 일반적인 UI 렌더링, 상태 변경 |