본문 바로가기

TIL

TIL - 리액트 컴포넌트 설계, SOLID 5원칙

1. SPR 단일 책임 원칙 (Single Responsibility Principle)

설명:

  • 단일 책임 원칙은 클래스나 컴포넌트가 한 가지 책임만 가져야 한다는 원칙입니다.
  • 변경 요청을 하는 사람(사용자나 이해관계자)에 따라 책임을 분리합니다.
  • 비즈니스 관점에서 책임을 나누어 한 가지 책임에 대한 변경사항이 있을 때만 코드가 수정되도록 합니다.

예시: 리액트 컴포넌트에서 사용자 프로필을 표시하는 부분을 예로 들겠습니다.

import React from 'react';
import UserInfo from './UserInfo';
import UserAvatar from './UserAvatar';

const UserProfile = ({ user }) => (
  <div>
    <UserAvatar avatarUrl={user.avatarUrl} />
    <UserInfo name={user.name} email={user.email} />
  </div>
);

export default UserProfile;

// UserInfo.js
const UserInfo = ({ name, email }) => (
  <div>
    <h2>{name}</h2>
    <p>{email}</p>
  </div>
);

export default UserInfo;

// UserAvatar.js
const UserAvatar = ({ avatarUrl }) => (
  <img src={avatarUrl} alt="User Avatar" />
);

export default UserAvatar;
  • UserProfile 컴포넌트는 사용자 정보와 아바타를 보여주는 책임만 가집니다.
  • 변경 요청이 있을 경우, 해당 책임에 맞는 컴포넌트만 수정하면 됩니다.

2. OCP 개방 폐쇄 원칙 (Open-Closed Principle)

설명:

  • 소프트웨어 구성 요소는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 합니다.
  • 기존 코드를 수정하지 않고 새로운 기능을 추가하는 방법을 지향합니다.

예시: CardItem 컴포넌트를 확장 가능한 구조로 만듭니다.

import React from 'react';

const CardItem = ({ Thumbnail, Body, Text }) => (
  <div className="card-item">
    {Thumbnail && <Thumbnail />}
    {Body && <Body />}
    {Text && <Text />}
  </div>
);

export default CardItem;

// Thumbnail.js
const Thumbnail = ({ imageUrl }) => <img src={imageUrl} alt="Thumbnail" />;
export default Thumbnail;

// Body.js
const Body = ({ content }) => <div>{content}</div>;
export default Body;

// Text.js
const Text = ({ text }) => <p>{text}</p>;
export default Text;

// Usage example
import CardItem from './CardItem';
import Thumbnail from './Thumbnail';
import Body from './Body';
import Text from './Text';

const App = () => (
  <CardItem 
    Thumbnail={() => <Thumbnail imageUrl="thumbnail.png" />} 
    Body={() => <Body content="Card content" />} 
    Text={() => <Text text="Some text" />} 
  />
);
  • CardItem 컴포넌트는 확장이 가능하지만, 기존 코드는 변경하지 않습니다.
  • 추가적인 섹션을 확장하여 추가할 수 있습니다.

3. LSP 리스코프 치환 원칙 (Liskov Substitution Principle)

설명:

  • 하위 타입 객체는 상위 타입 객체로 대체 가능해야 합니다.
  • 리액트에서는 상속보다는 합성을 사용하여 컴포넌트를 재사용하는 것이 권장됩니다.

예시: 상위 컴포넌트를 대체 가능한 하위 컴포넌트로 합성합니다.

const Button = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
);

export default Button;

// PrimaryButton.js
import Button from './Button';

const PrimaryButton = (props) => (
  <Button {...props} style={{ backgroundColor: 'blue', color: 'white' }} />
);

export default PrimaryButton;

// Usage example
import Button from './Button';
import PrimaryButton from './PrimaryButton';

const App = () => (
  <div>
    <Button onClick={() => alert('Button clicked')}>Click me</Button>
    <PrimaryButton onClick={() => alert('Primary button clicked')}>Primary</PrimaryButton>
  </div>
);
  • PrimaryButton은 Button을 대체하여 사용 가능합니다.
  • 컴포넌트 합성을 통해 코드를 재사용합니다.

4. ISP 인터페이스 분리 원칙 (Interface Segregation Principle)

설명:

  • 클라이언트(사용자)가 실제로 사용하는 인터페이스를 만들어야 합니다.
  • 불필요한 의존성을 줄이고, 인터페이스를 사용하는 용도에 맞게 분리합니다.

예시: 사용자 정보와 인증 인터페이스를 분리합니다.

export const getUserInfo = (userId) => {
  // Fetch user info from an API
};

// authService.js
export const signIn = (email, password) => {
  // Authenticate user
};
export const signOut = () => {
  // Sign out user
};

// Usage example
import { getUserInfo } from './userService';
import { signIn, signOut } from './authService';

const App = () => {
  // Use getUserInfo, signIn, and signOut as needed
};
  • userService와 authService를 분리하여 필요에 따라 사용할 수 있습니다.
  • 각 서비스는 사용 용도에 맞게 분리된 인터페이스를 제공합니다.

5. DIP 의존 역전 원칙 (Dependency Inversion Principle)

설명:

  • 고수준 모듈은 저수준 모듈의 구현에 의존하지 말아야 합니다.
  • 추상화된 레이어에 의존하여 의존성을 줄입니다.

예시: 데이터를 가져오는 로직을 추상화하여 컴포넌트에서 사용할 수 있도록 합니다.

// dataService.js
export const fetchData = async (url) => {
  const response = await fetch(url);
  const data = await response.json();
  return data;
};

// DataComponent.js
import React, { useEffect, useState } from 'react';
import { fetchData } from './dataService';

const DataComponent = ({ dataUrl }) => {
  const [data, setData] = useState(null);

  useEffect(() => {
    const loadData = async () => {
      const fetchedData = await fetchData(dataUrl);
      setData(fetchedData);
    };
    loadData();
  }, [dataUrl]);

  return (
    <div>
      {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>}
    </div>
  );
};

export default DataComponent;

// Usage example
const App = () => <DataComponent dataUrl="https://api.example.com/data" />;
  • fetchData 함수는 데이터를 가져오는 로직을 추상화합니다.
  • DataComponent는 데이터 로딩에 대한 로직을 알 필요 없이 fetchData에 의존하여 데이터를 가져옵니다.

이렇게 각 원칙에 따라 리액트 코드에서 어떻게 적용할 수 있는지 예시와 함께 설명드렸습니다. 각 원칙을 코드에 반영하면, 유지보수가 용이하고 재사용성이 높은 코드를 작성할 수 있습니다.