Tech/Clean Code

React OOP #1, setState props 지양하기

행복한 시지프 2025. 2. 16. 14:10

서언

안녕하세요. React OOP(Object-Oriented Programming) 시리즈 1번째 글을 씁니다. React 로 객체지향을 한다는 것을 사뭇 이해하기 어려울 수 있습니다. 그것은 객체지향에 대한 오해에서 비롯되는데요. 객체지향은 객체들간의 역할, 책임, 협력을 잘 명시하여 유지보수를 높이는 방법론입니다. React 에서 함수 컴포넌트도 객체이고, 여러 도메인 로직도 모두 객체일 것입니다. 각각의 컴포넌트가 어떤 역할과 책임을 갖고, 상호간에 어떻게 상호작용 할지를 생각하고 반영한다면, React 로 객체지향 프로그래밍을 하는 것이지요.

 

오늘은 React 로 OOP 하는 것에 익숙해지기 위해서, 익히 알려진 Anti-Pattern 에 대해 말하고자 합니다. 바로 “setState props 를 지양하기” 입니다. OOP 에서 setter 를 노출시키는 것이 지양되는 이유를 먼저 알아보고, 이를 React 측면에서 설명합니다.

 

OOP 에서 setter 를 노출시키는 것

OOP 에서는 setter 를 노출시키는 것은 일반적으로 지양됩니다. setter 를 노출시킨다는 것이 어떤 것인지 코드로 알아볼게요.

class Todo {
  constructor(title) {
    this.title = title;
  }

  setTitle(newTitle) {
    this.title = newTitle;
  }
}

이 코드에서 Todo 객체 외부에서 title 를 자유롭게 수정할 수 있습니다. title 이 어떤 식으로 들어와야 할지, validation 이 되지 않습니다. 더하여, Todo 를 쓰는 곳에서 어떤 식으로 update 하고 있을지 예측이 불가능 합니다.

 

이처럼 setter를 그대로 사용하는 것이 아니라, 함수명과 함수의 역할을 명시하고, 제한적으로 열어주는 것이 적절합니다.

class Todo {
  #title;
	
  constructor(title) {
    this.#title = title;
  }

  rename(newTitle) {
    if (typeof newTitle !== "string" || newTitle.length < 2) {
      throw new Error("할 일 제목은 2글자 이상이어야 합니다.");
    }
    this.#title = newTitle;
  }
}

setTitle 이 아니라, rename 이라는 이름으로 title 변경의 역할을 제한했습니다. 더하여, validation 의 책임을 Todo class 에 넣었습니다.

 

여기에 담긴 2가지 객체지향 원칙을 이야기해볼게요.

 

1. 캡슐화의 원칙

캡슐화란 객체 내부 상태를 외부에서 직접 접근하지 못 하도록 보호하는 개념입니다. 객체 field 를 private 처리하고, public method 를 통해서만 변경할 수 있도록 하는 것을 추구합니다.

 

외부에서 발생할 무분별한 변경을 차단하고, 예측한 방식으로만 변경되도록 합니다. setter 자체를 외부에 열어둔다면, 내부 상태가 예측 불가능하게 변경될 것입니다.

 

rename 이라는 메소드로 수정함으로써, title 을 은닉하고, 예측가능하게 변경됨을 보장합니다.

 

2. 책임의 분리

상태를 가진 객체가 상태의 변경을 관리할 책임을 가집니다. 무분별하게 상태가 변경되지 않도록 제한해주는 역할도 담당해야겠죠.

상태를 변경할 setter를 외부에 열어주고, 외부에서 제어권을 가지고 변경하는 것은 책임이 분산된 것입니다.

 

rename 이 validation 의 역할까지 포함함으로써, 제어 권한을 Todo가 갖고, 제한된 방식으로만 변경되도록 외부에 열어줍니다.

 

React 에서 setState 를 노출시키는 것

OOP 에서 왜 setter 를 노출시키는 것을 지양하는지 설명하였습니다. 이 똑같은 원칙이 React 에 적용됩니다. 익히 아는 “setState props 지양하기” 입니다.

 

setState 를 props 로 넘기는 예제를 살펴보겠습니다. 똑같이 title 을 변경하는 코드입니다.

import { useState } from "react";

function TodoApp() {
  const [todos, setTodos] = useState([{ id: 1, title: "React OOP #1 글쓰기", completed: false }]);

  return (
    <div>
      <h1>Todo List</h1>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} setTodos={setTodos} />
      ))}
    </div>
  );
}

function TodoItem({ todo, setTodos }) {
  const handleRename = () => {
    const newTitle = prompt("새로운 제목을 입력하세요:", todo.title);
    if (newTitle !== null) {
      setTodos((prevTodos) =>
        prevTodos.map((t) => (t.id === todo.id ? { ...t, title: newTitle } : t))
      );
    }
  };

  return (
    <div>
      <span>{todo.title}</span>
      <button onClick={handleRename}>이름 변경</button>
    </div>
  );
}

export default TodoApp;

위 코드는 상태는 TodoApp 에서 관리하지만, 직접적인 제어는 TodoItem 에서 하고 있습니다. setTodos 를 넘겨줌으로써 상태 제어권을 넘겨주고 있습니다.

 

위에서 설명한 캡슐화의 원칙, 책임의 분리 측면에서 이 코드를 비판해보도록 할게요.

 

1. 캡슐화의 원칙

위 코드에서 todos 상태 변경이 자유롭게 이루어집니다. 변경에 제한이 없으므로, 어떤 식으로든 변경이 가능합니다. 특정 todo 를 삭제해버릴 수 있습니다. completed 를 바꿔버릴 수 있습니다. title 을 어떤 식으로든 변경시킬 수 있습니다. 그러므로 예측하지 못한 udpate 가 발생할 수 있고, 유지보수가 어려워집니다.

 

2. 책임의 분리

상태를 가진 TodoApp 에서 상태 update 로직이 있지 않습니다. 상태를 관리할 책임을 가진 TodoApp 이 아닌, TodoItem 이 상태 변경 로직을 가지고 있습니다. 이는 잘못된 책임의 분리 입니다. TodoItem 이 CompletedTodoItem, InCompletedTodoItem 으로 분리된다면, 로직이 파편화되겠죠. 상태를 가진 쪽에서 제어 책임까지 가지는 것이 적절합니다.

 

이를 기반으로 코드를 수정해봅시다.

import { useState } from "react";

function TodoApp() {
  const [todos, setTodos] = useState([{ id: 1, title: "React OOP #1 글쓰기", completed: false }]);

  const renameTodo = (id, newTitle) => {
    if (typeof newTitle !== "string" || newTitle.trim().length < 2) {
      alert("할 일 제목은 최소 2글자 이상이어야 합니다.");
      return;
    }
    setTodos((prevTodos) =>
      prevTodos.map((todo) => (todo.id === id ? { ...todo, title: newTitle } : todo))
    );
  };

  return (
    <div>
      <h1>Todo List</h1>
      {todos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} renameTodo={renameTodo} />
      ))}
    </div>
  );
}

function TodoItem({ todo, renameTodo }) {
  const handleRename = () => {
    const newTitle = prompt("새로운 제목을 입력하세요:", todo.title);
    if (newTitle !== null) {
      renameTodo(todo.id, newTitle);
    }
  };

  return (
    <div>
      <span>{todo.title}</span>
      <button onClick={handleRename}>이름 변경</button>
    </div>
  );
}

export default TodoApp;

TodoApp 에서 renameTodo 를 정의함으로써, 상태를 캡슐화하고, 책임을 명확히 하였습니다. props 를 setTodos 가 아니라, renameTodo 을 넘겨줌으로써, 권한을 제한시켰습니다.

 

그러므로 TodoApp은 name 을 수정하는 기능만 열어두고 있으므로, 명확히 제어됩니다. 외부에서 잘못된 update 가 발생할 우려가 없습니다. 결국 더 예측 가능하고, 유지보수 하기 쉬운 코드가 됩니다.

 

마치며

프론트엔드 개발자가 어색한 개념인 OOP 와, 익숙한 “setState props 지양하기” 원칙을 엮어서 설명하였습니다. 간단한 원칙이지만, 이것이 어떤 상위 원칙에 기반하는가 설명하는 것이 저의 목표였습니다. 이런 식으로 React 를 클린하게 작성하는 원칙 다수가 객체지향 같은 소프트웨어 원칙에서 유래합니다. 앞으로도 OOP 를 React 에 어떻게 적용할 수 있을지 더 작성해 볼 계획입니다. 객체지향에 대한 오해를 풀고, 더 예측 가능하고 유지보수 하기 좋은 코드를 짤 수 있길 기대합니다.