리액트 컴포넌트 설계, 선언형이 답일까?

임홍원·2025년 3월 19일

최근회사에서 리액트 코드를 작성하다 보면 어떻게 하면 더 좋은 컴포넌트를 설계할 수 있을지 고민될 때가 많습니다.
특히 코드가 복잡해지고 유지보수가 어려워질수록, 더 나은 방법이 필요하다고 느끼게 됩니다.
이 글에서는 명령형(Imperative) 프로그래밍과 선언형(Declarative) 프로그래밍의 차이를 살펴보고,
어떤 방식이 리액트 컴포넌트 설계에 더 적합한지 고민해보려고 합니다.


명령형과 선언형 프로그래밍의 차이

프로그래밍을 할 때 코드를 작성하는 방식에는 크게 명령형(Imperative)선언형(Declarative) 두 가지 스타일이 있습니다.
각각의 차이를 이해하기 위해 간단한 배열 필터링 예제를 살펴보겠습니다.

명령형 방식 (Imperative)

명령형 프로그래밍에서는 어떻게(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문을 사용하여 짝수인지 확인 후 새로운 배열에 추가
  • 전체 흐름을 직접 제어해야 하므로 코드가 장황해질 가능성이 큼

선언형 방식 (Declarative)

선언형 프로그래밍에서는 무엇(What)을 원하는지만 선언하면, 내부적인 처리는 추상화된 함수가 알아서 수행합니다.

const numbers = [1, 2, 3, 4, 5];
const evenNumbers = numbers.filter(num => num % 2 === 0);

console.log(evenNumbers); // [2, 4]
  • filter()를 사용하여 간결하게 필터링
  • 내부적으로 for 루프를 사용하지만, 직접 작성할 필요 없음
  • 코드가 짧고 가독성이 뛰어나며 유지보수하기 쉬움

DOM 조작에서의 차이

명령형과 선언형의 차이는 DOM 조작에서도 확인할 수 있습니다.
Vanilla JavaScript와 React를 비교해보겠습니다.

명령형 방식 (Vanilla JS)

명령형 방식에서는 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 사용)

React를 사용하면 UI를 선언적으로 기술할 수 있습니다.

function App() {
  return (
    <button style={{ backgroundColor: "blue", color: "white" }} onClick={() => alert("Button clicked!")}>
      Click Me
    </button>
  );
}
  • JSX를 사용하여 버튼의 스타일과 이벤트를 한눈에 파악 가능
  • DOM을 직접 조작할 필요 없이 React가 알아서 관리
  • 코드가 간결하고 유지보수가 쉬움

리액트 컴포넌트에서의 적용

리액트 컴포넌트 설계에서도 명령형과 선언형의 차이가 명확하게 드러납니다.

명령형 방식 (Bad Code)

명령형 방식에서는 컴포넌트 내부에서 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의 컴포넌트 기반 개발 방식과 어울리지 않음

선언형 방식 (Best Code)

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 방식으로 상태 관리
  • UI를 선언적으로 작성하여 더 읽기 쉽고 유지보수하기 쉬움
  • React가 상태 변경에 따라 자동으로 UI를 업데이트

그렇지만 때때로 직접적으로 DOM을 조작해야 하는 경우

리액트에서는 대부분 선언형 방식을 따르는 것이 좋지만, 특정 상황에서는 명령형 방식이 필요할 수 있습니다.

상황선언형 가능?명령형 필요?해결 방법
포커스 설정❌ 불가능✅ 필요useRef + focus()
스크롤 이동❌ 불가능✅ 필요useRef + scrollIntoView()
Canvas, WebGL❌ 불가능✅ 필요useRef + getContext("2d")
외부 라이브러리❌ 불가능✅ 필요useRef + useEffect

즉, React에서는 "선언형을 기본으로, 명령형을 보조적으로" 사용하면 더 좋은 컴포넌트를 설계할 수 있습니다.

결론

리액트에서 선언형 프로그래밍을 사용하는 것이 더 나은 컴포넌트 설계로 이어진다는 점을 확인할 수 있었습니다.
하지만 때때로 명령형 방식이 필요한 경우도 존재하며, useRefuseEffect를 활용하여 직접 DOM을 조작해야 합니다.

요약

방식명령형(Imperative)선언형(Declarative)
코드 스타일절차 지향, 직접 명령추상화된 함수 활용
유지보수성복잡하고 어려움가독성이 좋고 유지보수 용이
적합한 경우포커스, 애니메이션, 외부 라이브러리일반적인 UI 렌더링, 상태 변경
profile
Frontend Developer

0개의 댓글