리액트 안티패턴: Setter를 하위 컴포넌트로 Prop으로 전달하는 패턴 지양하기 (번역)

Sangkun·2025년 1월 17일
1

리액트 안티패턴

목록 보기
2/2
post-thumbnail

이 글은 원저자의 허락을 맡아 <React Anti-Pattern: Stop Passing Setters Down the Components Tree>을 한국어로 번역한 글입니다.

Intro

최근에 저는 React 프로젝트에서 위험 신호를 보내는 몇 가지 패턴을 발견했습니다. 그 패턴 중 하나인 useState Setter를 프로퍼티로 전달하는 것을 살펴보겠습니다.

다음은 전형적인 예시입니다:

다음 예제는 순전히 설명 목적으로 양식 컴포넌트를 사용하며, 개념은 보편적으로 적용되며 양식 처리의 세부 사항과 관련이 없습니다.

// Form.jsx 
function Form() { 
    const [formData, setFormData] = useState({ name: '' }); 
    return (
        <div>
            <h1>Form</h1> 
            {/* setter 함수를 자식 컴포넌트로 전달 */}       
            <Input name={formData.name} setFormData={setFormData} />
            <button onClick={() => console.log(formData)}>Submit</button> 
        </div> 
    ); 
};

// Input.jsx
function Input({ name, setFormData }) { 
    const handleInputChange = (event) => { 
      // 부모 컴포넌트에게 받은 setter를 사용
        setFormData((prevData) => ({ ...prevData, name: event.target.value })); 
    }; 
    
    return ( 
        <div>
            <label> 
                Name: 
                <input type="text" value={name} onChange={handleInputChange} /> 
            </label>
        </div> 
    ); 
};

지금은 이 코드가 잘 작동하지만, 프로젝트가 발전하면서 왜 이런 방식이 문제를 일으킬 수 있는지 자세히 살펴보겠습니다.

The evolution of code

이제 Form을 개선해야 한다고 가정해 봅시다.
더 많은 필드가 추가되고 오류 처리가 도입되며 로직이 더 복잡해집니다.
이를 관리하기 위해 useState 에서 useReducer 로 전환하기로 결정합니다.
리팩터링된 Form 컴포넌트의 모습은 다음과 같습니다.

function reducer(state, action) { 
    switch(action.type) {
        case 'setField': 
            return { 
                ...state, 
                [action.payload.fieldName]: action.payload.fieldValue 
            };
        case 'setError':
            return {
                ...state,
                error: action.payload.error
            }
        default:
            return state;
    }
}

function Form() {  
    const [formData, dispatch] = useReducer(reducer, { name: '', error: null });
    return (
        <div>
            <h1>Form</h1> 
            {/* What happens to setFormData now? */}
            <Input name={formData.name} setFormData={setFormData} />
            <button onClick={() => console.log(formData)}>Submit</button> 
        </div> 
    ); 
};

상태 관리 메커니즘을 변경했지만 이제 useState를 중심으로 설계된 Input 컴포넌트의 로직이 더 이상 작동하지 않습니다.
이제 Input 컴포넌트에 dispatch 함수를 전달하여 특정 액션을 디스패치하도록 해야 할까요? 바로 이 지점에서 추상화 누수가 문제가 됩니다.

추상화 누수

추상화 누수는 한 컴포넌트가 다른 컴포넌트의 내부 구현에 대해 너무 많이 알고 있을 때 발생합니다.

이 경우 Input 컴포넌트는 다음과 같이 가정합니다

  1. 부모 컴포넌트가 useState를 사용하고 있습니다.
  2. 상태에는 다른 데이터와 함께 이름 필드가 직접 포함되어 있습니다.
  3. 부모 컴포넌트는 항상 동일한 상태 구조를 유지합니다.

이러한 가정으로 인해 자식 컴포넌트는 부모와 타이트하게 결합되므로 부모의 상태 구조나 관리 메커니즘이 변경되면 자식도 업데이트해야 합니다.

이것이 왜 나쁜건가요?

  • 취약성: 상위 컴포넌트의 로직을 변경하면 하위 컴포넌트가 손상되어 유지 관리에 골칫거리가 됩니다.
  • 재사용성 감소: 자식은 특정 부모 구현에 묶여 있어 다른 컨텍스트에서의 사용이 제한됩니다.
  • 명확성 손실: 원시 setState를 전달하면 자식 컴포넌트가 무엇을 수정해야 하는지 불분명해집니다.

누수를 어떻게 해결할 수 있을까요?

이 경우 추상화 누수를 해결하는 방법은 매우 간단합니다.
Input 컴포넌트는 실제 Setter 함수가 있는 프로퍼티를 받을 필요가 없습니다.
상태 변경을 캡슐화하는 콜백 함수를 가져올 수 있습니다.
예를 들어, handleNameChange 라는 함수는 다음과 같습니다.

// Form.jsx 
function Form() { 
    const [formData, setFormData] = useState({ name: '' }); 
    const handleNameChange = (name) => { 
        setFormData((prevState) => ({...prevState, name}));
    };
    
    return (
        <div>
            <h1>Form</h1> 
            {/* Pass the setter function down to Input */}
            <Input name={formData.name} onChange={handleNameChange} />
            <button onClick={() => console.log(formData)}>Submit</button> 
        </div> 
    ); 
};

// Input.jsx 
function Input({ name, onChange }) { 
    const handleInputChange = (event) => { 
        onChange(event.target.value);
    }; 
    
    return ( 
        <div>
            <label> 
                Name: 
                <input type="text" value={name} onChange={handleInputChange} /> 
            </label>
        </div> 
    ); 
};

요약

이 글에서는 useStateSetter 함수를 Props로 전달하는 것이 좋은 방법이 아닌 이유에 대해 설명했습니다.
그렇게 하면 자식 컴포넌트가 부모의 구현에 긴밀하게 결합되어 추상화 누수가 발생합니다.
대신 캡슐화된 콜백 함수를 사용하면 명확성, 재사용성, 유지보수성이 어떻게 향상되는지 보여드렸습니다.

profile
지식에 대한 두려움을 기록으로 극복하는 개발자

0개의 댓글

관련 채용 정보