단일 책임 원칙 ⭐️⭐️⭐️

단일 책임 원칙은 솔리드원칙의 핵심이라고 생각합니다.

이게 기반이 되어 있지 않으면 모든게 다 어그러 지는 느낌입니다.

하나의 클래스에서 하나의 역할만을 가지고 있어야하지 유기적으로 협력할 수 있습니다.

하나의 객체 혹은 클래스에서 여러가지 역할을 하고 있다면 어떤 곳에서 어떤 업무를 수행할지 중간 제어자가

확실하게 제어하기 쉽지 않습니다.

프론트엔드에서도 이 원칙을 지키며 프로젝트를 진행하기 위해서는 우선적으로 컴포넌트가 정의된 역할 기반으로 작성되어 있어야 합니다. 그리고 내부적인 컨벤션으로 그 범위를 지정해야 하죠.

저는 콘웨이법칙의 효율성을 매우 신뢰합니다. 어플리케이션 구조는 해당 조직의 업무 소통 구조를 따라야 한다는고 생각합니다.

만약 기획 디자인 개발 이렇게 3개의 직군끼리 협업하는 구조의 프로젝트를 하고 있다고 가정하면 보통은 기획에서 해당 기능의 명세를 작성해주게 됩니다.

저희는 이런 구조를 따라서 해당 역할을 가진 객체 혹은 컴포넌트 모듈을 설계하면 됩니다. 네이밍 규칙도 왠만하면 맞춰서 소통 이슈가 없도록하면 좋죠. 네이밍 규칙 통일은 생각보다 중요하다고 생각합니다.

기획에서 정의된 기능 역할로 인터페이스를 설계하고, 디자인 팀에서 설계한 디자인 가이드 대로 프리젠테이션 컴포넌트를 설계하게 되면 자연스럽게 단일 책임 원칙을 지키는 코드를 작성할 수 있게 됩니다.

만약 조직 적으로 디자인과 기획이 같이 움직이거나 합쳐진 구조라면 저라면

디자인 시스템을 구지 만들지 않는것이 더 효율적으로 업무를 진행할 수 있는 방법이라고 생각합니다.

단일 책임 원칙은 매우 간단한 원칙이지만 지키기 어렵고 핵심적이라고 생각합니다.

폐쇄 개방 원칙⭐️⭐️

확장은 가능하고 수정을 불가능한 객체 클래스를 만들어야 한다는 원칙입니다.

이원칙이 지키고 해결하고자 하는 것은,

인터페이스 상속을 통해 이 원칙을 지킬 수 있습니다.

프론트엔드에서도 이원칙을 지키게 되면 컴포넌트 재상용 성을 많이 높일 수 있게 됩니다.

그리고 저도 이쁜? 코드를 작성하는데 이 원칙은 필수적이라고 생각합니다.

예시로는 요소 컴포넌트 디자인시스템 개발시에 컴포넌트의 스타일은 인터페이스로 받을 수 있고 해당 인터페이스에 의해서 요소 컴포넌트의 스타일이 정해지게 됩니다.

// ❌ 나쁜 예: 수정에 열려있는 버튼
function Button({ variant, children }) {
  // 새로운 스타일을 추가할 때마다 함수를 수정해야 함
  const getStyle = () => {
    if (variant === "primary") {
      return "bg-blue-500 text-white";
    } else if (variant === "secondary") {
      return "bg-gray-500 text-white";
    } else if (variant === "warning") {
      return "bg-yellow-500 text-black";
    }
    // 새로운 variant를 추가할 때마다 여기를 수정해야 함
  };

  return <button className={getStyle()}>{children}</button>;
}

// ✅ 좋은 예: 확장에 열려있는 버튼
const BUTTON_VARIANTS = {
  primary: "bg-blue-500 text-white",
  secondary: "bg-gray-500 text-white",
  warning: "bg-yellow-500 text-black",
  // 새로운 스타일을 추가할 때 여기만 추가하면 됨
  success: "bg-green-500 text-white",
  danger: "bg-red-500 text-white",
};

function Button({ variant = "primary", children }) {
  return <button className={BUTTON_VARIANTS[variant]}>{children}</button>;
}

폼 유효성 검사 로직또한 인터페이스 상속 패턴으로 작업하면 깔끔하게

컴포넌트를 작성할수 있습니다.

// ❌ 나쁜 예: 유효성 검사 로직이 컴포넌트 내부에 하드코딩
function RegistrationForm() {
  const validateForm = (values) => {
    const errors = {};

    if (!values.email.includes("@")) {
      errors.email = "유효한 이메일을 입력하세요";
    }
    if (values.password.length < 6) {
      errors.password = "비밀번호는 6자 이상이어야 합니다";
    }
    // 새로운 규칙을 추가할 때마다 함수를 수정해야 함

    return errors;
  };

  // ... 폼 로직
}

// ✅ 좋은 예: 유효성 검사 규칙을 주입받는 방식
const emailValidator = (value) =>
  !value.includes("@") ? "유효한 이메일을 입력하세요" : null;

const passwordValidator = (value) =>
  value.length < 6 ? "비밀번호는 6자 이상이어야 합니다" : null;

const phoneValidator = (value) =>
  !/^\d{3}-\d{4}-\d{4}$/.test(value) ? "올바른 전화번호 형식이 아닙니다" : null;

function FormField({ value, validator, onChange }) {
  const [error, setError] = useState(null);

  const handleChange = (e) => {
    const newValue = e.target.value;
    const validationError = validator(newValue);
    setError(validationError);
    onChange(newValue);
  };

  return (
    <div>
      <input value={value} onChange={handleChange} />
      {error && <span className="text-red-500">{error}</span>}
    </div>
  );
}

// 사용 예시
function RegistrationForm() {
  return (
    <form>
      <FormField value={email} validator={emailValidator} onChange={setEmail} />
      <FormField
        value={password}
        validator={passwordValidator}
        onChange={setPassword}
      />
      <FormField value={phone} validator={phoneValidator} onChange={setPhone} />
    </form>
  );
}

리스코프 치환 원칙

자식 클래스가 부모클래스를 대체할수 있어야 한다는 원칙입니다.

프로그램상 같은 인터페이스를 공유하더라도 부모클래스를 자식 클래스가 대체할수 없는 이상 동작을 하게 되면 동작을 예측하기 어렵고 버그를 발생시킬수 있습니다.

예를 들어 버튼 컴포넌트를 만든다고 가정하면 부모 기본 요소 버튼 컴포넌트를 상속 받은 상태 검사용 버튼 컴포넌트를 만들었다고 했을때 버튼 컴포넌트의 이벤트 핸들러 속성인 온클릭을 자식 컴포넌트와 부모 컴포넌트가 같은 동작으로 처리되어 있지 않다면, 같은 인터페이스를 공유하기 때문에 오류가 나지 않지만 버그를 유발할수 있습니다.

프론트엔드 컴포넌트 개발에서는 보통을 잘 지켜지고 있는 원칙이라서 쉽게 넘어가도 좋을것 같습니다.

// ✅ 기본 버튼 컴포넌트 (부모)
function Button({ children, onClick, disabled = false }) {
  return (
    <button onClick={onClick} disabled={disabled} className="px-4 py-2 rounded">
      {children}
    </button>
  );
}

// ✅ 특수 목적 버튼들 (자식) - LSP 준수
function PrimaryButton(props) {
  return (
    <Button {...props} className="px-4 py-2 rounded bg-blue-500 text-white" />
  );
}

function SubmitButton({ isLoading, ...props }) {
  return (
    <Button {...props} disabled={isLoading || props.disabled}>
      {isLoading ? <LoadingSpinner /> : props.children}
    </Button>
  );
}

// 사용 예시
function Form() {
  // 어떤 버튼을 사용하든 동일한 방식으로 작동
  return (
    <form>
      <Button onClick={() => console.log("clicked")}>일반 버튼</Button>
      <PrimaryButton onClick={() => console.log("clicked")}>
        중요 버튼
      </PrimaryButton>
      <SubmitButton onClick={() => console.log("clicked")}>제출</SubmitButton>
    </form>
  );
}
// ✅ 기본 입력 필드 컴포넌트 (부모)
function InputField({ value, onChange, error, label, ...props }) {
  return (
    <div>
      {label && <label>{label}</label>}
      <input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className={`border rounded p-2 ${
          error ? "border-red-500" : "border-gray-300"
        }`}
        {...props}
      />
      {error && <span className="text-red-500 text-sm">{error}</span>}
    </div>
  );
}

// ✅ 특수 목적 입력 필드들 (자식) - LSP 준수
function NumberInput({ value, onChange, ...props }) {
  return (
    <InputField
      {...props}
      type="number"
      value={value}
      onChange={(val) => {
        const num = Number(val);
        if (!isNaN(num)) {
          onChange(num);
        }
      }}
    />
  );
}

function EmailInput(props) {
  return (
    <InputField
      {...props}
      type="email"
      pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
    />
  );
}

// 사용 예시
function RegistrationForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    age: 0,
  });

  // 모든 입력 필드가 동일한 방식으로 작동
  return (
    <form>
      <InputField
        label="이름"
        value={formData.name}
        onChange={(value) => setFormData({ ...formData, name: value })}
      />
      <EmailInput
        label="이메일"
        value={formData.email}
        onChange={(value) => setFormData({ ...formData, email: value })}
      />
      <NumberInput
        label="나이"
        value={formData.age}
        onChange={(value) => setFormData({ ...formData, age: value })}
      />
    </form>
  );
}

인터페이스 분리 원칙

클래스가 본인이 사용하는 역할에 맞는 인터페이스만 의존하도록 한다는 원칙입니다.

해당 원칙은 단일 책임 원칙을 지킨다면 어느정도 쉽게 지켜 질수 있습니다. 컴포넌트는 동작하는 특징 혹은 역할에 따라 인턴페이스를 의존해서 본인의 역할을 정의합니다.

프론트엔드 컴포넌트 개발에서 이원칙을 지키게 되면 합성 컴포넌트 패턴으로 코드를 깔끔하게 작성할 수 있습니다.

// 기본 테이블 셀
function TableCell({ children }) {
  return <td className="p-2 border">{children}</td>;
}

// 정렬 가능한 헤더
function SortableHeader({ title, onSort, sortDirection }) {
  return (
    <th className="p-2 border cursor-pointer" onClick={onSort}>
      {title} {sortDirection === 'asc' ? '' : ''}
    </th>
  );
}

// 필터링 가능한 헤더
function FilterHeader({ title, onFilter }) {
  const [value, setValue] = useState('');

  return (
    <th className="p-2 border">
      <div>{title}</div>
      <input
        value={value}
        onChange={e => {
          setValue(e.target.value);
          onFilter(e.target.value);
        }}
        placeholder="Filter..."
      />
    </th>
  );
}

// 선택 가능한 행
function SelectableRow({ children, onSelect, isSelected }) {
  return (
    <tr
      onClick={onSelect}
      className={isSelected ? 'bg-blue-100' : ''}
    >
      {children}
    </tr>
  );
}

// 실제 사용 예시
function DataTable({ data, config }) {
  return (
    <table>
      <thead>
        <tr>
          {config.map(column => (
            column.sortable ? (
              <SortableHeader
                key={column.key}
                title={column.title}
                onSort={() => column.onSort(column.key)}
                sortDirection={column.sortDirection}
              />
            ) : column.filterable ? (
              <FilterHeader
                key={column.key}
                title={column.title}
                onFilter={column.onFilter}
              />
            ) : (
              <th key={column.key}>{column.title}</th>
            )
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map(row => (
          config.selectable ? (
            <SelectableRow
              key={row.id}
              onSelect={() => config.onSelect(row)}
              isSelected={config.selectedIds.includes(row.id)}
            >
              {config.map(column => (
                <TableCell key={column.key}>
                  {row[column.key]}
                </TableCell>
              ))}
            </SelectableRow>
          ) : (
            <tr key={row.id}>
              {config.map(column => (
                <TableCell key={column.key}>
                  {row[column.key]}
                </TableCell>
              ))}
            </tr>
          )
        ))}
      </tbody>
    </table>
  );
}

 컴포넌트 예시

javascriptCopy// 기본 입력 필드 인터페이스
function TextField({ label, value, onChange, error }) {
  return (
    <div>
      <label>{label}</label>
      <input
        value={value}
        onChange={e => onChange(e.target.value)}
        className={error ? 'border-red-500' : ''}
      />
      {error && <span className="text-red-500">{error}</span>}
    </div>
  );
}

// 유효성 검사 기능
function withValidation(WrappedComponent) {
  return function ValidatedField({ validate, ...props }) {
    const [error, setError] = useState(null);

    const handleChange = (value) => {
      const error = validate?.(value);
      setError(error);
      props.onChange(value);
    };

    return (
      <WrappedComponent
        {...props}
        error={error}
        onChange={handleChange}
      />
    );
  };
}

// 자동완성 기능
function withAutocomplete(WrappedComponent) {
  return function AutocompleteField({ suggestions, ...props }) {
    const [showSuggestions, setShowSuggestions] = useState(false);

    return (
      <div>
        <WrappedComponent {...props} />
        {showSuggestions && (
          <ul>
            {suggestions.map(suggestion => (
              <li
                key={suggestion}
                onClick={() => {
                  props.onChange(suggestion);
                  setShowSuggestions(false);
                }}
              >
                {suggestion}
              </li>
            ))}
          </ul>
        )}
      </div>
    );
  };
}

// 조합하여 사용
const ValidatedTextField = withValidation(TextField);
const AutocompleteTextField = withAutocomplete(TextField);
const ValidatedAutocompleteField = withAutocomplete(withValidation(TextField));

// 사용 예시
function RegistrationForm() {
  return (
    <form>
      <TextField
        label="이름"
        value={name}
        onChange={setName}
      />

      <ValidatedTextField
        label="이메일"
        value={email}
        onChange={setEmail}
        validate={value => !value.includes('@') ? '유효한 이메일을 입력하세요' : null}
      />

      <AutocompleteTextField
        label="도시"
        value={city}
        onChange={setCity}
        suggestions={['서울', '부산', '대구', '인천']}
      />
    </form>
  );
}

의존 관계 역전 원칙

추상화에 의존한 클래스로 작성해야 한다는 원칙. 쉽게 말해 상위 클래스가 하위 클래스를 의존하지 않아야 하고, 둘다 추상화에 의존해야 한다는 것입니다.

리액트개발에서는 기본적으로 단방향으로 props를 통해 데이터가 흐르게 되고 부모 컴포넌트에서 자식컴포넌트의 작업을 대신 처리해 줘야 하는 상황이 빈번하게 발생하기도 합니다.

우리는 이런 문제를 해결하기 위해 외부에서 객체에 새로운 기능을 추가하는 의존성 주입 패턴을 사용하거나, 컨텍스트 패턴을 활용해서 정보를 컨텍스트에 저장하고 관리하며 하위 컴포넌트에서는 해당 컨텍스트 정보를 가져와서 사용하게 됩니다.

컴포넌트에서 어떤 인터페이스를 의존해서 작업을 하게 되는 것이기 때문에 컴포넌트 자체에서는 해당 기능에 대한 책임을 가지지 않아도 됩니다.

// ❌ 나쁜 예: 컴포넌트가 직접 API를 호출하는 경우
function UserList() {
  const [users, setUsers] = useState([]);

  useEffect(() => {
    // 컴포넌트가 직접 API에 의존
    fetch('api/users')
      .then(res => res.json())
      .then(setUsers);
  }, []);

  return (/* 렌더링 로직 */);
}

// ✅ 좋은 예: 의존성 주입을 사용한 구현
// 1. 서비스 인터페이스 정의
interface UserService {
  getUsers(): Promise<User[]>;
  getUserById(id: string): Promise<User>;
}

// 2. 구체적인 구현
class ApiUserService implements UserService {
  async getUsers() {
    const res = await fetch('api/users');
    return res.json();
  }

  async getUserById(id) {
    const res = await fetch(`api/users/${id}`);
    return res.json();
  }
}

class MockUserService implements UserService {
  async getUsers() {
    return [
      { id: 1, name: 'Test User' }
    ];
  }

  async getUserById(id) {
    return { id, name: 'Test User' };
  }
}

// 3. 커스텀 훅으로 추상화
function useUsers(userService: UserService) {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    userService
      .getUsers()
      .then(setUsers)
      .catch(setError)
      .finally(() => setLoading(false));
  }, [userService]);

  return { users, loading, error };
}

// 4. 컴포넌트
function UserList({ userService }) {
  const { users, loading, error } = useUsers(userService);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 5. 사용 예시
function App() {
  const userService = new ApiUserService();
  // 또는 테스트용: const userService = new MockUserService();

  return <UserList userService={userService} />;
}
// 1. 테마 인터페이스 정의
interface Theme {
  colors: {
    primary: string,
    secondary: string,
    background: string,
    text: string,
  };
  spacing: {
    small: string,
    medium: string,
    large: string,
  };
}

// 2. 테마 프로바이더 컴포넌트
function ThemeProvider({ theme, children }) {
  return (
    <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
  );
}

// 3. 테마를 사용하는 컴포넌트
function Button({ children, variant = "primary" }) {
  const theme = useTheme();

  return (
    <button
      style=
    >
      {children}
    </button>
  );
}

// 4. 구체적인 테마 구현
const lightTheme: Theme = {
  colors: {
    primary: "#007bff",
    secondary: "#6c757d",
    background: "#ffffff",
    text: "#000000",
  },
  spacing: {
    small: "0.5rem",
    medium: "1rem",
    large: "2rem",
  },
};

const darkTheme: Theme = {
  colors: {
    primary: "#0056b3",
    secondary: "#495057",
    background: "#121212",
    text: "#ffffff",
  },
  spacing: {
    small: "0.5rem",
    medium: "1rem",
    large: "2rem",
  },
};

// 5. 사용 예시
function App() {
  const [isDarkMode, setIsDarkMode] = useState(false);
  const theme = isDarkMode ? darkTheme : lightTheme;

  return (
    <ThemeProvider theme={theme}>
      <Button variant="primary">클릭하세요</Button>
      <button onClick={() => setIsDarkMode(!isDarkMode)}>테마 전환</button>
    </ThemeProvider>
  );
}