Javascript

React에서 콜백을 활용한 컴포넌트 분리 이해하기

드리프트2 2024. 9. 21. 13:23

React에서 콜백을 활용한 컴포넌트 분리 이해하기

React를 사용하다 보면, 컴포넌트를 어떻게 나누고 서로 소통할지 고민하게 됩니다.

 

특히, 부모 컴포넌트와 자식 컴포넌트 간의 상태 관리와 데이터 흐름은 중요한 주제입니다.

 

이번 글에서는 콜백(callback)을 활용하여 컴포넌트를 더 깔끔하고 재사용 가능하게 분리하는 방법을 초보자도 쉽게 이해할 수 있도록 자세히 설명하겠습니다.

 

예제 시나리오

우선, 간단한 예제를 통해 설명을 시작해보겠습니다. 온보딩(가입 안내) 과정을 단계별로 표시하는 컴포넌트를 만든다고 가정해보겠습니다.

 

온보딩 과정은 다음과 같은 세 단계로 이루어져 있습니다:

  1. 환영 인사 단계 (WelcomeStep)
  2. 서비스 약관 동의 단계 (TermsOfServiceStep)
  3. 완료 단계 (CompleteStep)

이제 이 세 가지 단계를 표시하는 OnboardingSection 컴포넌트를 만들어 보겠습니다.

 

초기 구현: 상태와 자식 컴포넌트 연결

초보 개발자는 다음과 같이 구현할 수 있습니다:

import React, { useState } from 'react';
import { Box, Button } from '@chakra-ui/react';

export type Step = 'welcome' | 'terms-of-service' | 'complete';

export default function OnboardingSection() {
  const [step, setStep] = useState<Step>('welcome');

  return (
    <Box>
      {step === 'welcome' && (
        <WelcomeStep setStep={setStep} />
      )}
      {step === 'terms-of-service' && (
        <TermsOfServiceStep setStep={setStep} />
      )}
      {step === 'complete' && <CompleteStep />}
    </Box>
  );
}

function WelcomeStep(props: { setStep: (newStep: Step) => void }) {
  return (
    <Box>
      <Box>우리 플랫폼에 오신 것을 환영합니다!</Box>
      <Box>
        <Button onClick={() => props.setStep('terms-of-service')}>다음</Button>
      </Box>
    </Box>
  );
}

function TermsOfServiceStep(props: { setStep: (newStep: Step) => void }) {
  return (
    <Box>
      <Box>서비스 약관에 동의해주세요.</Box>
      <Box>
        <Button onClick={() => props.setStep('complete')}>동의</Button>
      </Box>
    </Box>
  );
}

function CompleteStep() {
  return (
    <Box>
      <Box>온보딩이 완료되었습니다!</Box>
    </Box>
  );
}

 

위 코드에서 OnboardingSection 컴포넌트는 현재 단계를 step 상태로 관리하고 있으며, 각 단계에 따라 다른 자식 컴포넌트를 렌더링합니다.

 

자식 컴포넌트인 WelcomeStepTermsOfServiceStep은 부모의 setStep 함수를 받아 상태를 변경합니다.

 

문제점: 부모와 자식 컴포넌트의 밀접한 결합

이 접근 방식에는 몇 가지 문제가 있습니다:

  1. 부모 상태가 자식에게 드러남 (State Exposure):
    부모 컴포넌트인 OnboardingSection이 가지고 있는 setStep 함수를 자식 컴포넌트에게 직접 전달하면, 자식 컴포넌트가 부모의 단계를 마음대로 바꿀 수 있게 됩니다. 마치 집의 열쇠를 모두에게 주는 것과 비슷해서, 누군가가 마음대로 집 안을 움직일 수 있게 되는 거죠. 이렇게 되면 부모 컴포넌트의 내부 상태가 외부에 노출되어 관리가 어려워집니다.
  2. 컴포넌트 간의 너무 강한 연결 (Increased Coupling):
    자식 컴포넌트가 부모 컴포넌트의 상태를 직접 바꾸려면, 두 컴포넌트가 서로 깊게 연결되어야 합니다. 이는 마치 두 사람이 항상 함께 움직여야 하는 것과 같아서, 하나를 바꾸면 다른 하나도 같이 바꿔야 하는 불편함이 생깁니다. 이런 강한 연결은 컴포넌트를 재사용하기 어렵게 만들고, 나중에 수정하거나 업데이트할 때 많은 문제를 일으킬 수 있습니다.
  3. 테스트가 복잡해짐 (Testing Difficulties):
    컴포넌트들이 강하게 연결되어 있으면, 각각의 컴포넌트를 따로따로 테스트하기가 어려워집니다. 예를 들어, 자식 컴포넌트를 테스트하려면 항상 부모 컴포넌트도 함께 테스트해야 해서, 테스트 과정이 복잡해집니다. 이는 버그를 찾거나 기능을 추가할 때 시간을 많이 소비하게 만듭니다.

 

해결책: 콜백 함수를 통한 상태 관리

이러한 문제를 해결하기 위해 콜백 함수를 활용하여 부모가 자식에게 행동을 제어할 수 있게 합니다.

 

이를 통해 컴포넌트 간의 결합도를 낮추고, 각 컴포넌트를 더 독립적으로 관리할 수 있습니다.

 

개선된 구현: 콜백을 이용한 상태 전환

먼저, OnboardingSection 컴포넌트는 상태를 관리하고, 자식 컴포넌트에게 필요한 콜백 함수를 전달합니다.

import React, { useState } from 'react';
import { Box, Button } from '@chakra-ui/react';

export type Step = 'welcome' | 'terms-of-service' | 'complete';

export default function OnboardingSection() {
  const [step, setStep] = useState<Step>('welcome');
**
  // 다음 단계로 이동하는 함수
  const goToTerms = () => setStep('terms-of-service');
  const goToComplete = () => setStep('complete');

  return (
    <Box>
      {step === 'welcome' && (
        <WelcomeStep onClickNext={goToTerms} />
      )}
      {step === 'terms-of-service' && (
        <TermsOfServiceStep onClickNext={goToComplete} />
      )}
      {step === 'complete' && <CompleteStep />}
    </Box>
  );
}

 

이제 자식 컴포넌트는 부모로부터 전달받은 onClickNext 콜백을 사용하여 다음 단계로 이동할 수 있습니다.

function WelcomeStep(props: { onClickNext: () => void }) {
  return (
    <Box>
      <Box>우리 플랫폼에 오신 것을 환영합니다!</Box>
      <Box>
        <Button onClick={props.onClickNext}>다음</Button>
      </Box>
    </Box>
  );
}

function TermsOfServiceStep(props: { onClickNext: () => void }) {
  return (
    <Box>
      <Box>서비스 약관에 동의해주세요.</Box>
      <Box>
        <Button onClick={props.onClickNext}>동의</Button>
      </Box>
    </Box>
  );
}

function CompleteStep() {
  return (
    <Box>
      <Box>온보딩이 완료되었습니다!</Box>
    </Box>
  );
}

장점 설명

  1. 상태를 부모에게만 맡기기:
    이제 OnboardingSection 컴포넌트만 상태를 관리하고, 자식 컴포넌트는 부모에게 필요한 행동을 요청하는 역할을 합니다. 예를 들어, 자식 컴포넌트는 "다음 단계로 가주세요"라고 부모에게 말만 하면 됩니다. 이렇게 하면 자식 컴포넌트가 부모의 내부 상태를 직접 변경할 수 없게 되어, 부모의 상태가 안전하게 유지됩니다.
  2. 컴포넌트 간의 연결을 약하게 만들기:
    자식 컴포넌트는 부모의 상태 관리 함수에 의존하지 않고, 단순히 부모가 제공하는 버튼 클릭 같은 신호를 보냅니다. 예를 들어, 자식 컴포넌트는 "다음" 버튼을 클릭했을 때 부모에게 "다음 단계로 이동해줘"라고 요청만 합니다. 이렇게 하면 컴포넌트들이 서로 독립적으로 움직일 수 있어, 재사용이 쉽고 유지보수가 편리해집니다.
  3. 테스트하기 쉬워짐:
    자식 컴포넌트는 단순히 버튼 클릭 같은 행동을 부모에게 알리는 역할만 하므로, 테스트할 때 부모 컴포넌트를 신경 쓸 필요가 없습니다. 자식 컴포넌트를 따로 테스트할 수 있어, 버그를 쉽게 찾고 기능을 확실하게 만들 수 있습니다.
  4. 추가 기능을 쉽게 넣을 수 있음:
    부모 컴포넌트에서 콜백 함수를 정의하면, 상태를 변경하기 전에 추가적인 작업을 쉽게 넣을 수 있습니다. 예를 들어, 다음 단계로 넘어가기 전에 사용자에게 메시지를 보여주거나, 서버에 데이터를 저장하는 등의 작업을 할 수 있습니다. 이렇게 하면 컴포넌트의 기능을 더욱 확장하고, 다양한 상황에 유연하게 대처할 수 있습니다.

 

확장된 예제: 추가 로직 포함하기

더 복잡한 로직을 추가하여 콜백 함수의 활용 방식을 살펴보겠습니다.

 

예를 들어, 사용자가 서비스 약관에 동의하기 전에 해당 약관을 검토하는 로직을 추가할 수 있습니다.

export default function OnboardingSection() {
  const [step, setStep] = useState<Step>('welcome');

  const goToTerms = () => {
    // 예를 들어, 로그를 남기거나 API 호출을 추가할 수 있습니다.
    console.log('Welcome step completed.');
    setStep('terms-of-service');
  };

  const goToComplete = () => {
    // 추가적인 검증이나 API 호출을 할 수 있습니다.
    console.log('Terms of Service accepted.');
    setStep('complete');
  };

  return (
    <Box>
      {step === 'welcome' && (
        <WelcomeStep onClickNext={goToTerms} />
      )}
      {step === 'terms-of-service' && (
        <TermsOfServiceStep onClickNext={goToComplete} />
      )}
      {step === 'complete' && <CompleteStep />}
    </Box>
  );
}

 

이렇게 하면, 각 콜백 함수 내에서 필요한 추가 작업을 수행한 후 상태를 변경할 수 있습니다.

 

이는 컴포넌트의 책임을 명확하게 분리하고, 유지보수를 용이하게 합니다.

 

컴포넌트 설계 시 인터페이스 신경 쓰기

컴포넌트를 설계할 때는 항상 다음 사항을 고려하세요:

  1. 명확한 인터페이스 정의: 컴포넌트가 외부에 어떤 데이터를 제공하고, 어떤 동작을 수행할지를 명확하게 정의합니다. 예를 들어, 단순히 onClickNext와 같은 콜백을 제공하여 부모가 상태를 관리할 수 있게 합니다.
  2. 재사용성 고려: 컴포넌트를 특정 부모에 종속시키지 않고, 다양한 상황에서 재사용할 수 있도록 설계합니다. 이는 콜백 패턴을 활용함으로써 쉽게 달성할 수 있습니다.
  3. 독립적인 테스트 가능성: 컴포넌트를 독립적으로 테스트할 수 있도록 설계합니다. 상태 관리 로직을 부모에게 위임하면, 자식 컴포넌트는 단순히 콜백을 호출하는 역할만 하므로 테스트가 쉬워집니다.

 

결론

React에서 컴포넌트 간의 결합도를 낮추기 위해 콜백 함수를 활용하는 것은 매우 유용한 방법입니다.

 

이를 통해 부모 컴포넌트는 상태를 일관되게 관리할 수 있으며, 자식 컴포넌트는 더 독립적이고 재사용 가능하게 됩니다.

 

컴포넌트를 설계할 때는 항상 인터페이스를 명확하게 정의하고, 각 컴포넌트의 책임을 분리하여 유지보수성과 테스트 용이성을 높이는 노력을 기울이세요.

 

이러한 접근 방식을 통해 더욱 견고하고 확장 가능한 React 애플리케이션을 구축할 수 있습니다.