나만보는 업무정리

리액트 공통컴포넌트 체크리스트

Developer Mobssie 2024. 5. 13. 07:59

 
 
명확한 인터페이스 설계: 컴포넌트의 props는 명확하고 이해하기 쉬워야 합니다.
구조 분해 할당을 사용하면 명확한 인터페이스 설계를 통해 컴포넌트의 사용법을 쉽게 파악할 수 있습니다.
코드의 가독성이 향상되며, 오류의 가능성을 줄일 수 있습니다.

// 좋은 예
const Button = ({ label, onClick }) => (
  <button onClick={onClick}>{label}</button>
);
// 나쁜 예
const Button = (props) => (
  <button onClick={props.handleClick}>{props.text}</button>
);

 
예를 들어 { label, onClick }을 사용하는 경우, 이 props가 Button 컴포넌트에서 어떻게 사용되는지 바로 알 수 있습니다.
label은 버튼에 표시될 텍스트를 나타내고, onClick은 버튼 클릭 시 실행할 함수를 나타냅니다.
반면, props로 전체를 받아서 사용하는 경우 (props.handleClick, props.text 같이), 이 props들이 구체적으로 어떤 역할을 하는지 파악하기 위해 추가적인 코드 분석이 필요할 수 있습니다.
 


 
 
Props 전달 최소화: 컴포넌트에 전달하는 props는 최소화하여, 각 컴포넌트의 역할과 책임을 명확하게

// 좋은 예
const UserProfile = ({ user }) => (
  <div>
    <Avatar image={user.avatar} />
    <UserName name={user.name} />
  </div>
);


// 나쁜 예
const UserProfile = ({ avatar, name }) => (
  <div>
    <Avatar image={avatar} />
    <UserName name={name} />
  </div>
);

 
Button 컴포넌트에서는 간단하고 명확한 인터페이스를 유지하는 것이 중요하지만
UserProfile에서는 user 객체와 같이 복합적인 데이터 구조를 효율적으로 다루는 것이 중요합니다.
user 객체를 통째로 넘겨 주어, 나중에 user 객체 내 다른 속성이 필요할 때 추가적인 props 변경 없이 사용할 수 있어야 합니다.
 


 
 
 
단일 책임 원칙 준수: 각 컴포넌트는 하나의 기능만 수행해야 합니다.
코드의 복잡성을 줄이고, 유지보수를 용이하게 만들며, 시스템의 확장성을 향상시키는 데 기여합니다.

// 좋은 예
const LoginForm = () => (
  <form>
     <UsernameInput />
     <PasswordInput />
     <LoginButton />
  </form> 
);

// 나쁜 예
const LoginForm = () => (
   <form>
     <label>
       Username:
       <input type="text" name="username" />
     </label>
     <label>
       Password:
       <input type="password" name="password" />
     </label>
     <button type="submit">Login</button>
   </form>
);

 
* 첫번째 예제 : LoginForm 컴포넌트가 오직 로그인 폼을 구성하는 데 집중하고 있고, 'UsernameInput', 'PasswordInput', 'LoginButton'과 같은 하위 컴포넌트를 사용하여 각각의 입력 필드와 로그인 버튼을 독립적으로 관리합니다.
 
* 두번째 예제 LoginForm 컴포넌트 자체 내에서 모든 요소를 직접 처리합니다. 이 방식은 간단한 폼에서는 작업하기 쉽지만, 각 요소를 수정하거나 확장할 때 전체 폼을 다루어야 하므로 유지보수가 더 복잡해질 수 있습니다.
 
첫 번째 방식은 각 요소를 독립적으로 관리하고 확장하는 데 더 효과적입니다.
 
 


 
 
 
재사용 가능성 고려: 컴포넌트는 재사용 가능하도록 유연하게 설계해야 합니다.
 

// 좋은 예
const Button = ({ type = 'button', children, ...props }) => (
   <button type={type} {...props}>
     {children}
   </button> 
);

// 나쁜 예
const SubmitButton = () => (
   <button type="submit">Submit</button>
);

SubmitButton 컴포넌트는 오직 'submit' 유형의 버튼으로만 사용되고, 버튼의 내용도 'Submit'으로 고정하면 컴포넌트의 사용 가능성이 크게 제한되며, 다른 컨텍스트나 다른 텍스트를 요구하는 상황에서는 새로운 컴포넌트를 만들어야 합니다.
 
Button 컴포넌트는 type 속성의 기본값을 'button'으로 설정하고, 다른 값으로 쉽게 변경할 수 있는 기능은 React 컴포넌트에서 프롭스를 활용하여 구현할 수 있습니다. 이는 컴포넌트의 인스턴스를 생성할 때 type 프롭스에 원하는 값을 전달함으로써 가능합니다.
* type은 기본값이 'button'이지만, 'submit', 'reset' 등 다른 값으로 쉽게 변경가능
* children을 사용하여 버튼의 내용을 자유롭게 정의
* props를 통해 className, onClick 같은 이벤트 핸들러나 스타일 등의 속성을 자유롭게 추가가 가능
 
 


 
 
테스트 가능한 컴포넌트 작성: 컴포넌트는 쉽게 테스트할 수 있도록 작성해야 합니다.

// 좋은 예 (유연한 초기 상태 설정)
export const Counter1 = ({ initialValue = 0 }) => {
  const [count, setCount] = React.useState(initialValue);
   return (
     <div>
       <button onClick={() => setCount(count + 1)}>Increase</button>
       <p>{count}</p>
     </div> 
   );
};

// 나쁜 예 (고정된 초기 상태 설정)
export const Counter2 = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <p>{count}</p>
    </div>
  );
};

 
* Counter1 컴포넌트는 initialValue prop을 받아서 초기 상태 값을 설정할 수 있습니다.
이는 테스트 중에 다양한 초기 값으로 컴포넌트를 쉽게 테스트할 수 있게 합니다.

<Counter initialValue={10} />

 
* Counter2 컴포넌트는 초기 상태 값을 고정적으로 0으로 설정하고 있어, 다른 초기 상태 값을 가지고 테스트를 수행하기 어렵습니다.
개발 과정에서나 다른 상황에서 컴포넌트를 재사용할 때 제한적일 수 있습니다.
 
 


 
 
 
의존성 최소화: 컴포넌트는 외부 의존성을 최소화해야 합니다.

// 좋은 예
// 데이터를 가져오는 로직을 포함하지 않기 때문에, 그 자체로는 어떠한 외부 데이터 의존성도 갖지 않습니다.
const UserAvatar = ({ user }) => (
  <img src={user.avatarUrl} alt={user.name} />
);
// 나쁜 예
// userId가 변경될 때마다 데이터를 새로 불러오는 로직을 내부적으로 포함하고 있기 때문에, 
// 컴포넌트의 사용이 그 상황에 맞춰져 있고, 데이터를 가져오는 API의 의존성이 큽니다.
const UserAvatar = ({ userId }) => {
const [user, setUser] = React.useState(null);
  React.useEffect(() => {
    fetchUserById(userId).then(setUser);
  }, [userId]);
  if (!user) return <div>Loading...</div>;
  return <img src={user.avatarUrl} alt={user.name} />;
};

 
 


 
 
 
스타일 캡슐화: 컴포넌트의 스타일은 독립적이어야 하며, 외부 스타일의 영향을 받지 않도록 설계합니다.

// 좋은 예 (CSS 클래스를 사용한 스타일링)
const Button = ({ children, variant = 'primary' }) => (
  <button className={`button button-${variant}`}>{children}</button>
);

// 나쁜 예 (인라인 스타일을 사용한 스타일링)
const Button = ({ children }) => (
  <button style={{ backgroundColor: 'blue', color: 'white' }}>{children}</button>
);

 
첫번째 예시 버튼 컴포넌트는 variant prop을 받아서 이를 기반으로 다양한 스타일을 적용할 수 있습니다.
className은 "button" 과 "button-{variant}"의 조합을 사용합니다. 
CSS 파일에 정의된 스타일을 사용하여, 컴포넌트의 스타일이 일관되고, 중복을 피하며, 스타일 변경이 필요할 경우 CSS 파일만 수정하면 되기 때문에 유지보수가 쉽습니다.
 
두번째 예시 버튼 컴포넌트 인라인 스타일을 사용하여 버튼의 배경색과 텍스트 색상을 직접 지정합니다.
이 방식은 스타일 변경이 빈번하지 않고, 특정 컴포넌트에만 적용되는 간단한 스타일링에 적합할 수 있습니다.
그 상황이 아니라면 인라인 스타일은 스타일 정보를 자바스크립트 파일 내부에 포함시키므로 CSS 캐싱의 이점을 활용할 수 없고,
스타일 변경 시 여러 컴포넌트의 코드를 각각 수정해야 할 수 있습니다.
 
첫번째 예시인 CSS 클래스를 사용한 스타일링을 적용했을때
styled-components와 같은 CSS-in-JS 라이브러리를 사용하여 스타일을 컴포넌트에 직접 캡슐화합니다.
이 방식은 스타일을 완전히 독립적으로 만들고, props를 통해 동적으로 스타일을 조정할 수 있어 유연합니다.
컴포넌트의 스타일이 외부 CSS의 영향을 받지 않으며, 테마나 상태에 따라 스타일을 쉽게 변경가능합니다.

import styled from 'styled-components';

const StyledButton = styled.button`
  background-color: ${(props) => props.theme.primary};
  color: white;
  padding: 10px;
  border: none;
  border-radius: 5px;

  &:hover {
    background-color: ${(props) => props.theme.primaryDark};
  }
`;

const Button = ({ children }) => (
  <StyledButton>{children}</StyledButton>
);

 
 


 
 
 
 
컴포넌트의 상태 관리를 명확히: 상태 관리는 내부적으로 캡슐화하여 컴포넌트 외부에서 쉽게 이해할 수 있도록 해야 합니다.
Toggle 컴포넌트는 자신의 상태를 내부적으로 관리하고 있습니다.
다른 컴포넌트와의 상태 공유나 외부에서의 상태 조작 없이 독립적으로 기능하게 됩니다.

// 좋은 예
const Toggle = () => {
  const [isOn, setIsOn] = React.useState(false);
  const toggle = () => setIsOn(!isOn);
  return <button onClick={toggle}>{isOn ? 'ON' : 'OFF'}</button>;
};

// 나쁜 예
const Toggle = ({ isOn, toggle }) => (
  <button onClick={toggle}>{isOn ? 'ON' : 'OFF'}</button>
);

 
상태가 공유되어야 하는 상황에서는 이 방식이 비효율적일 수 있는데 아래와 같습니다.
 
상태 lifting을 적절히 사용

const ParentComponent = () => {
  const [activeTab, setActiveTab] = React.useState(0);
  return (
    <div>
      <Tabs activeTab={activeTab} setActiveTab={setActiveTab} />
      <TabContent activeTab={activeTab} />
    </div>
  );
};

상태 공유: ParentComponent activeTab이라는 상태를 가지며, 이를 Tabs 컴포넌트와 TabContent 컴포넌트와 공유합니다. 이 구조는 activeTab 상태가 Tabs 컴포넌트에서 변경되었을 때, 그 변경사항이 TabContent에도 반영됩니다. 상태를 상위 컴포넌트에서 관리함으로써, 상태 관련 로직이 한 곳에 집중하는게 용이해지고, 상태를 필요로 하는 다른 컴포넌트에 쉽게 전달할 수 있습니다.
 


 
 
컴포넌트의 불변성 유지: 컴포넌트의 상태는 불변성을 유지하도록 처리해야 합니다.
불변성을 유지한다는 것은 데이터 구조를 직접 수정하지 않고, 새로운 데이터 구조를 생성하여 갱신하는 것을 의미합니다.

// 좋은 예
const AddItem = ({ items, setItems }) => {
  const [itemName, setItemName] = React.useState("");
  const addItem = () => {
    setItems([...items, itemName]); // 불변성 유지
    setItemName("");
  };
  return (
    <div>
      <input
        value={itemName}
        onChange={(e) => setItemName(e.target.value)}
        type="text"
      />
      <button onClick={addItem}>Add Item</button>
    </div>
  );
};

불변성 유지: addItem 함수에서 items 배열을 직접 변경하지 않고, 새 배열을 생성하여 itemName을 추가합니다. 이는 원래 배열을 복사하고 ([...items]), 새 항목을 배열의 끝에 추가합니다. React가 상태 변경을 감지하고 컴포넌트를 적절하게 재렌더링하도록 합니다.

  • 왜 중요한가?: 불변성을 유지하면 React의 최적화된 상태 관리와 렌더링 메커니즘이 효과적으로 작동합니다. 또한, 미래의 복잡한 버그를 방지하고, 상태의 변경 이력을 추적하기 쉬워집니다.
// 나쁜 예
const AddItem = ({ items, setItems }) => {
  const [itemName, setItemName] = React.useState("");
  const addItem = () => {
    items.push(itemName); // 직접적인 props 변경
    setItems(items); // 문제의 소지가 있음
    setItemName("");
  };
  return (
    <div>
      <input
        value={itemName}
        onChange={(e) => setItemName(e.target.value)}
        type="text"
      />
      <button onClick={addItem}>Add Item</button>
    </div>
  );
};

불변성 위반: 이 예에서 items.push(itemName)는 기존 items 배열에 직접 아이템을 추가합니다. 이러한 방식은 items 배열의 원본을 변경하기 때문에 React가 상태의 변경을 제대로 감지하지 못할 수 있습니다.
왜냐하면 배열 객체의 참조가 변경되지 않았기 때문입니다. 결과적으로, 이로 인해 UI가 제대로 업데이트되지 않을 수 있습니다.
 


 
컴포넌트의 사이드 이펙트 제어: 사이드 이펙트는 useEffect 등을 통해 관리해야 합니다.
useEffect를 사용하여 사이드 이펙트를 제어하는 것은 컴포넌트의 생명주기에 따른 작업을 정리하고, 리소스의 낭비를 방지하는 데 중요

// 좋은 예 (useEffect를 통한 사이드 이펙트 제어)
const Timer = () => {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timerId = setInterval(() => setTime(time + 1), 1000);
    return () => clearInterval(timerId);
    // clearInterval을 호출함으로써 컴포넌트가 언마운트 되거나 업데이트 되기 전에 타이머를 정확히 제거
  }, [time]);
  //  [time]에 time을 넣어주어서, time이 변할 때마다 이펙트가 다시 실행
  
  return <div>{time} seconds</div>;
};

// 나쁜 예 (제어되지 않은 사이드 이펙트)
const Timer = () => {
  const [time, setTime] = React.useState(0);
  setInterval(() => setTime(time + 1), 1000);  // Uncontrolled side effect
  return <div>{time} seconds</div>;
};

setIntervaluseEffect 내부에서 관리되지 않기 때문에, 컴포넌트가 언마운트된 후에도 인터벌이 계속 실행될 수 있습니다.
이는 메모리 누수를 유발하고 애플리케이션의 성능을 저하시킬 수 있습니다.
 


 
 
 
컴포넌트가 표현하는 데이터의 타입 체크하기: 타입 체크를 통해 컴포넌트에 전달되는 데이터가 올바른 형태인지 검증

interface User {
  name: string;
  age: number;
}

interface UserProfileProps {
  user: User;
}

const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.age} years old</p>
    </div>
  );
};

 


 
 
효율적 상태 관리: 상태가 필요한 경우, 최소한으로 유지하고 가능하다면 계산된 상태를 사용합니다.
React에서는 상태 관리를 위해 useState 또는 다른 상태 관리 훅(useReducer, useContext 등)을 사용하는 것이 필수입니다.
상태의 변화가 UI에 자동으로 반영되도록 하고, 상태 변화를 효과적으로 관리됩니다.
 
useState를 사용하여 count 상태를 관리하면 상태가 업데이트될 때마다 컴포넌트가 자동으로 리렌더링되도록 보장됩니다.
컴포넌트의 initialValue prop을 통해 초기 카운트 값을 설정하면 재사용성을 높이고, 다양한 초기 상태를 갖는 카운터를 쉽게 구현 됩니다.

// 좋은 예 (올바른 상태 관리)
const Counter = ({ initialValue }) => {
  const [count, setCount] = React.useState(initialValue);
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <p>Count: {count}</p>
    </div>
  );
};
// 나쁜 예 (부적절한 상태 관리)
const Counter = () => {
  let count = 0; // This is not reactive and not recommended in React
  const increase = () => {
    count++;
    console.log(count); // This will not trigger a re-render
  };
  return (
    <div>
      <button onClick={increase}>Increase</button>
      <p>Count: {count}</p>
    </div>
  );
};

 
count++가 실행되어도 React는 이를 감지하지 못하며, 따라서 값이 변경되어도 컴포넌트가 리렌더링되지 않습니다.
count 값이 변경되더라도, 그 변경 사항이 UI에 반영되지 않습니다.
 


 
 
 
데이터 플로우를 명확히 하고 단방향으로 유지: 데이터는 항상 상위에서 하위 컴포넌트로 흐르도록 설계

// ParentComponent
const ParentComponent = () => {
  const [message, setMessage] = React.useState("Initial message");

  const changeMessage = () => {
    setMessage("Message updated by child");
  };

  return (
    <div>
      <h1>{message}</h1>
      <ChildComponent onChangeMessage={changeMessage} />
    </div>
  );
};
// ChildComponent
// 자식 컴포넌트는 부모 컴포넌트로부터 함수를 prop으로 받아, 해당 함수를 통해 부모 컴포넌트의 상태를 변경
const ChildComponent = ({ onChangeMessage }) => {
  return (
    <button onClick={onChangeMessage}>Change Message</button>
  );
};

모든 상태 변경 로직이 부모 컴포넌트에 있어서 자식 컴포넌트는 독립적이고 예측 가능한 동작을 유지할 수 있습니다.
 

// 나쁜 예
// ChildComponent
// 자식 컴포넌트가 자체적으로 상태를 변경하려고 하면 단방향 데이터 플로우 원칙에 어긋
const ChildComponent = ({ message, setMessage }) => {
  const localChangeMessage = () => {
    setMessage("Message updated by child");
  };

  return (
    <button onClick={localChangeMessage}>Change Message</button>
  );
};.

 
모든 상태 변경이 부모 컴포넌트를 통해 이루어져야 하며, 자식 컴포넌트는 상태를 직접 변경하지 않는다는 단방향 데이터 플로우의 원칙따라야 합니다. 데이터 흐름을 명확하게 하고, 상태 관리를 더 쉽게 할 수 있도록 도와줍니다.
 


 
 
 

'나만보는 업무정리' 카테고리의 다른 글

ESlint, Prettier 설정 방법  (0) 2021.04.22
Admin 시작 (회사구조)  (0) 2020.11.05