본문 바로가기

3주차

[React] - 개발 단계에서 고민해보면 좋은 것들(상태, 컴포넌트)

안녕하세요👋 웹파트 OB 김건휘입니다. 오늘은 리액트 중 개발 단계에서 실질적으로 고민해보면 좋은 주제 들에 대한 이야기를 해보고자 합니다.

1. State Management의 최소화

리액트에서 정말 중요한 개념 중 하나인 "state(상태)"를 어떻게 관리할 것인가에 대한 고민은 웹 개발을 하면서 마주하게되는 고민 중 하나인데요. 

 

1️⃣ state(상태)가 뭔데?

리액트에서 상태(state)란, 컴포넌트의 동적 데이터를 저장하고 관리하는 객체이다. 상태(state)는 컴포넌트가 변경될 수 있는 값들을 보유하는 데 사용되며, UI가 그에 맞춰 자동으로 업데이트해준다. 즉, 상호작용에 따라 변하는 값들이 상태(state)이다.

 

2️⃣ state(상태) 특징

1. 컴포넌트의 기억 저장소

  • 상태는 컴포넌트가 기억해야 하는 데이터나 정보를 담고 있다. 예를 들어, 버튼을 클릭했을 때 카운트가 증가하는 상황에서 이 카운트 값은 상태로 관리할 수 있다.

2. UI의 동적 업데이트

  • 상태는 컴포넌트가 처음 렌더링된 후에도 업데이트될 수 있다. 상태가 변할 때마다 리액트는 해당 상태를 사용하는 컴포넌트를 자동으로 리렌더링해서 UI를 최신 상태로 유지해준다. 이 과정을 통해 상태와 UI가 항상 동기화된다.

3. setState로 관리

  • 리액트에서는 상태를 직접 수정하지 않고, 상태를 변경하기 위해 setState (함수형 컴포넌트에서는 useState)를 사용한다. 이를 통해 리액트가 상태 변경을 감지하고 렌더링을 트리거할 수 있도록 해준다.

4. 지역성

  • 상태는 기본적으로 해당 컴포넌트 내부에서만 유효하며, 다른 컴포넌트에서는 접근할 수 없다. 하지만, 필요에 따라 상태를 하위 컴포넌트에 props로 전달할 수 있다.

🧐상태가 뭔지 알았어. 그런데 왜 상태(state)를 최소화 해야되는거야?

리액트 공식문서에서도 상태(state)는 최소화 할수록 좋다고 명시되어 있는데요. 이는 코드의 복잡도를 줄이고, 불필요한 렌더링을 방지하며, 관리와 유지보수를 쉽게 하기 위함이라고 할 수 있다.

 

✔️ 우리가 상태로 관리하는 데이터가 상태로 관리할 필요 없을 수도 있다

제가 개발할 때 가장 첫번째로 중요시 하는 부분중 하나이기도 한데요. 프로젝트의 볼륨이 점점 커지면서 걷잡을 수 없을정도로 많은 useState들로 상태가 꼬이고 꼬여서 잘되던 동작도 안되고, 원인을 찾는 과정에서 상태를 추적하기 어렵워지게 되는데요.

불필요한 상태가 많아진다는 것은 불필요한 리렌더링이 발생하는 것과 같으니 당연히 성능 측면에서도 좋지 않겠죠. 상태로 관리해야 하는 데이터인지, 아니면 계산 가능한 값인지 구분하여, 계산할 수 있는 값은 상태로 두지 말고 변수로 관리하고, 필요한 순간에 계산해서 사용하도록 하는 방식을 최우선으로 고려하는 것이 어떨까요?

 

✔️상태를 최상위 컴포넌트에만 위치시키기

상태는 최상위 컴포넌트에 위치시키고, 필요한 경우에만 하위 컴포넌트로 내려주는 방식이 좋다고 리액트 공식문서에서 설명하고 있습니다. 이렇게 하면 불필요한 상태 공유가 줄어들고, 상태가 필요한 컴포넌트가 많아져도 관리하기가 쉬워진다고 한다.

 

🧐궁금증: 상태를 최상위 컴포넌트에만 위치시키게 되면 최상위 컴포넌트에서 너무 많은 상태들이 존재하고 너무 많은  props를 전달하게 되지 않아?

매우 맞는 말이다. 상태는 필요한 범위의 가장 가까운 상위 컴포넌트에 위치시키는 것이 좋다. 상태가 너무 많은 컴포넌트에 전달될 필요가 없다면, 해당 상태는 최상위가 아닌 중간 수준의 컴포넌트에 두어도 된다. 상태를 위치시키는 기준은 공유해야 하는 범위에 맞추는 것이 좋다. => 공유 범위를 최소화 할 수 있는 컴포넌트에서 상태를 관리하는 것이 point

 

✔️커스텀 훅 적극 활용하기

특정 상태와 로직을 여러 컴포넌트에서 재사용해야 하는 경우, 해당 로직을 커스텀 훅으로 분리하여 사용하는 것을 권장한다. 상태 로직을 컴포넌트 바깥으로 빼내어 가독성을 높이고, 필요할 때마다 훅을 호출하여 같은 상태 관리 패턴을 재사용할 수 있다. 내가 구현한 로직을 커스텀 훅으로 분리하여 재사용할 수 있는지 항상 고민하자. => 커스텀 훅을 잘활용하면 상태 관리의 복잡성을 줄일 수 있다.

 

2. 컴포넌트 설계

개발 초기 단계에서 "컴포넌트를 어떻게 설계할까?"에 대한 고민을 매 프로젝트 마다 하게된다. 

해당 내용은 리액트 공식문서에서 발췌해온 내용이다.

 

위의 공식문서 내용에도 알 수 있듯이 컴포넌트 설계에 있어 단일 책임 원칙이라는 테크닉을 제시하고 있다.

✔️단일 책임 원칙(Single Responsibility Principle)

더보기

함수나 클래스는 한가지 기능만 수행해야 한다.

컴퓨터 전공이라면 다들 한번씩은 들어봤을 내용이다. 컴포넌트의 관점에서 보면 하나의 컴포넌트는 한가지 기능만 수행해야한다는 것이다.

여기서 오해하기 쉬운 것이 자칫 '단일 기능'으로 오해할 수 있다는 것이다. 리액트 컴포넌트 설계에서 단일 책임 원칙을 단일 기능으로 이해하는 것은 완벽한 해석은 아니다. 실제로는 '단일 책임'을 좀 더 포괄적이고 의미 있는 역할로 해석하는 것이 적절하다. 단순히 하나의 기능을 수행한다는 의미보다는, 컴포넌트가 하나의 명확한 '역할'을 갖도록 설계하는 것이 핵심이다.

 

예를 들어, 단순히 "버튼"을 생각해보면 "클릭" 기능만 처리하는 것 같지만, 실제 버튼 컴포넌트는 외형 스타일링, 접근성, 사용자 피드백(활성화, 비활성화 등) 같은 여러 책임이 포함됩니다. 단일 책임 원칙에 따라 이 컴포넌트가 "버튼 역할"을 수행하는 것이라면, 이와 관련된 시각적 요소와 상호작용을 관리하는 모든 것을 포괄하는 역할을 가지게 되는것이다.

핵심은 컴포넌트가 특정 역할이나 의도를 잘 표현하고, 그 역할 외의 책임을 가지지 않도록 하는 것입니다. 예를 들어, 데이터를 가져오는 로직을 UI 컴포넌트와 분리해 훅(hook)으로 빼내는 것도 같은 맥락에서 이루어지는 작업이라고 할 수 있다.

따라서 단일 책임 원칙을 "단일 기능"보다는 "단일 목적" 또는 "명확한 역할 수행"으로 해석하면 컴포넌트의 책임을 더 명확하게 정의할 수 있습니다.

 

✔️예시 코드

단일 책임을 적용하지 않은 컴포넌트

// FetchButton.jsx
import React, { useState, useEffect } from 'react';

const FetchButton = () => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setLoading(true);
    try {
      const response = await fetch('https://api.example.com/data');
      const result = await response.json();
      setData(result);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, []);

  return (
    <button onClick={fetchData} disabled={loading}>
      {loading ? 'Loading...' : 'Fetch Data'}
    </button>
  );
};

export default FetchButton;

이 컴포넌트는 버튼을 렌더링하면서 데이터를 가져오는 로직까지 포함하고 있다.

위의 코드에서는 버튼을 렌더링하는 컴포넌트가 데이터 가져오기 로직까지 함께 담당하고 있다. 이렇게 하면 UI와 데이터 로직이 결합되기 때문에, 이 컴포넌트의 책임이 명확하지 않고 재사용하기 어려워진다.

 

단일 책임을 적용한 컴포넌트

1. 데이터를 가져오는 책임을 커스텀 훅 useFetchData로 분리.

2. Button 컴포넌트는  데이터를 가져오는 책임은 포함하지 않고, UI와 이벤트 처리만 담당하도록 분리.

 

// useFetchData.js
import { useState, useEffect } from 'react';

const useFetchData = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);

  const fetchData = async () => {
    setLoading(true);
    try {
      const response = await fetch(url);
      const result = await response.json();
      setData(result);
    } catch (error) {
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchData();
  }, [url]);

  return { data, loading, fetchData };
};

export default useFetchData;

 

 

// Button.jsx
import React from 'react';

const Button = ({ onClick, loading }) => (
  <button onClick={onClick} disabled={loading}>
    {loading ? 'Loading...' : 'Fetch Data'}
  </button>
);

export default Button;

 

// FetchButtonContainer.jsx
import React from 'react';
import Button from './Button';
import useFetchData from './useFetchData';

const FetchButtonContainer = () => {
  const { data, loading, fetchData } = useFetchData('https://api.example.com/data');

  return (
    <div>
      <Button onClick={fetchData} loading={loading} />
      {data && <div>Data: {JSON.stringify(data)}</div>}
    </div>
  );
};

export default FetchButtonContainer;
  • useFetchData 훅은 데이터를 가져오는 로직만 담당.
  • Button 컴포넌트는 순수하게 UI와 클릭 이벤트만 처리.
  • FetchButtonContainer 컴포넌트는 useFetchData를 이용해 버튼과 데이터를 함께 조합하고, 버튼 클릭 시 데이터를 가져오도록 한다.

이를 통해 각 컴포넌트의 책임이 명확해지고 재사용성이 높아진다. 또하느 유지보수가 쉬워지며 데이터를 가져오는 로직을 다양한 곳에서 재활용할 수 있다.

 

🧐궁금증: 단일 책임 원칙을 통해 각 컴포넌트가 담당하는 역할과 책임을 명확히 함으로써, 코드의 가독성을 높이고, 재사용성을 증가시킬 수 있다는 것은 납득이가. 그렇게되면 결국 많은 컴포넌트가 만들어지게 될텐데, 컴포넌트를 관리하기 어려워지는거 아니야?

맞는 말이다. 프로젝트의 볼륨이 점점 커지고, 디벨롭 과정에서 단일 책임 원칙을 적용하다보면 많은 컴포넌트로 인해 구조를 명확하게 파악하기 힘들고, 빠르게 내가 지금 수정하고 싶은 컴포넌트가 무엇인지 파악하기 힘든 경우가 생긴다. 막연하게 단일 책임 원칙이 복잡성을 증가시킨다고 생각하지말고 아래에서 제시하는 방법들을 고려해서 컴포넌트를 관리하는건 어떨까? 

 

  • 폴더 구조 설계
    컴포넌트의 폴더 구조를 역할에 따라 나눈다. 예를 들어, 페이지별로 폴더를 구분하거나, 재사용 가능한 UI 컴포넌트와 페이지별 컴포넌트를 분리하는 방식으로 구조화한다.
  • 컴포넌트 네이밍 규칙
    일관된 네이밍 규칙을 적용하면 역할이 비슷한 컴포넌트끼리 그룹화된 느낌을 줄 수 있다. 예를 들어, ButtonPrimary, ButtonSecondary와 같이 역할이 명확히 드러나는 네이밍을 적용하여, 여러 버튼 컴포넌트가 있어도 구분이 쉽도록 한다.
  • 스토리북(Storybook) 사용
    스토리북 같은 도구를 사용하여 각 컴포넌트의 상태와 사용법을 문서화하고 시각화할 수 있다. 특히 디자이너들과 소통하기도 편리하다. 또한, 내가 지금 수정하고 싶은 컴포넌트가 무엇인지 빠르게 찾을 수 있다.

 

해당 방법들을 고수해 나가기 위해서는 팀원들과 컨벤션을 정하고, 컨벤션을 지키기 위해서 노력하는 자세 또한 중요할 것이다. 

 

 

단순히, 기능 명세서에서 제시하는 로직과 동일하게 동작하고, 피그마에 있는 디자인과 동일하게 퍼블리싱을 하는것이 아닌 어떻게 프로젝트의 복잡성을 낮출 수 있을지, 책임을 명확하게 할 수 있을지 고민하는 개발자가 되기 위해 노력하자.

 

 

 

 

 

'3주차' 카테고리의 다른 글

[React] 다양한 렌더링 방식 - CSR, SSR, SSG  (0) 2024.11.02
[React] StrictMode  (0) 2024.11.01
[React] Custom Hook  (0) 2024.11.01
[React] useEffect 훅과 의존성 배열  (0) 2024.11.01
리액트 컴포넌트의 Lifecycle  (0) 2024.11.01