22.11.27 학습일기

22.11.27 학습일기

Facts

변경에 용이한 소프트웨어를 만들기 위해 고민해보았는데 oop 에서는 어떻게 하고 있는지 찾아보니 solid 원칙이라는 것을 알게 되었다. react 에서 어떻게 oop의 solid 원칙을 적용할 수 있을까를 고민하는 시간을 가졌다.

우선 solid 가 무엇인지에 대해 찾아보았다.

  • SRP(Single Responsibility Principle): 단일 책임 원칙

    • 하나의 객체는 반드시 하나의 동작만의 책임을 갖는다.

  • OCP(Open Closed Principle): 개방 폐쇄 원칙

    • 객체의 확장은 열려 있고, 수정은 닫혀있어야 한다.

  • LCP(Liskov Substition Principle): 리스코프 치환 원칙

    • 부모 객체를 호출하는 동작에서 자식 객체가 부모 객체를 완전히 대체할 수 있어야 한다.

  • ISP(Interface Segregation Principle): 인터페이스 분리 원칙

    • 객체는 자신이 호출하지 않는 메소드에 의존하지 않아야 한다.

  • DIP(Dependency Inversion Principle): 의존성 역전 원칙

    • 객체는 인터페이스에 의존해야한다.

    • 즉 의존 관계를 맺을 때 변화하기 쉬운 것(구현) 보다는, 변화하지 않는 것(인터페이스)에 의존해야한다.

리액트에서 위의 원칙들을 어떻게 적용해볼 수 있을지 간단한 예제들을 생각해봤다.

  • SRP

    • 데이터와 관련된 로직은 custom hook 으로, ui 와 관련된 로직은 컴포넌트로 분리한다.

    • 하나의 도메인에 묶일 수 있도록 한다.

      • 별점

      • 블로그 리스트

    • 관련 훅과 컴포넌트가 하나로 합쳐져 있어도 상관없을 것 같다.

export function App() {
  const { rating, onChangeRating } = useRating();
  const { blogs } = useBlogs();

  return (
    <main>
     <Rating rating={rating} onChangeRating={onChangeRating}/>
     <BlogCards blogs={blogs}/>
    </main>
  )
}
  • OCP

    • 기존 코드를 변경하지 않고 수정을 할 수 있도록 한다.

    • 조건에 의해 컴포넌트의 내부 로직이 계속해서 변경된다면 위험할 수 있다.

// before
export function CommentEditor({ user }) {
  // user.role 이 추가되면 분기가 계속해서 생기게 된다.

  if(user.role === 'ADMIN') {
    return <textarea placeholder="운영자 계정입니다."></textarea>
  }

  if(user.role === 'B2B') {
    return <textarea placeholder="비지니스 유저는 댓글을 달 수 없습니다."></textarea>
  }

  return <textarea placeholder="댓글을 작성해주세요."></textarea>
}

// after
export function CommentEditor({ placeholder }) {
  return <textarea placeholder={placeholder}></textarea>
}

export function AdminCommentEditor() {
  return <CommentEditor placeholder="운영자 계정입니다.">
}

export function B2BCommentEditor() {
  return <CommentEditor placeholder="비지니스 유저는 댓글을 달 수 없습니다.">
}

export function UserCommentEditor() {
  return <CommentEditor placeholder="댓글을 작성해주세요.">
}
  • LCP

    • 아직 예제가 잘 생각나지 않음.

  • ISP

    • 컴포넌트 props 를 넘겨줄 때 책임을 지지 않는 영역의 데이터까지 넘기지 않는다.

//before
export function Career() {
  const { data: career } = useCareerQuery();

  return <section>
   <Name career={career} />
   <PhoneNumber career={career} />
   <Email career={career} />
   <Adreess career={career} />
  </section>
}

// after
export function Career() {
  const { data: career } = useCareerQuery();

  return <section>
   <Name name={career.name} />
   <PhoneNumber phoneNumber={career.phoneNumber} />
   <Email email={career.email} />
   <Adreess address={career.address} />
  </section>
}
  • DIP

    • 세부 구현에 의존하지말고 인터페이스를 만들어서 의존하게 한다. 리액트에서는 props들의 인터페이스들을 활용할 수 있을 것 같다.

    • example1 에서 onSumbit 이 Form 컴포넌트 내부에 있다면 다른 로직이 반영된 onSumbit 이 필요할 때 코드 변경이 매우 어렵다. 즉 Form 컴포넌트가 내부의 onSubmit이라는 세부구현에 의존하게 되면 변경이 어렵다는 것이다. 이것을 인터페이스에 의존하게 만들면 변경에 용이한 컴포넌트가 될 수 있다.

    • example2 은 react-query 에서 제공하는 provider 예제이다. context-api를 활용하여 의존성 주입을 해주고 있는데 나중에 테스트코드를 작성할 때 client 인터페이스를 맞춘 Stub 클래스를 주입해주면 테스트할 때 용이할 수 있다. 나중에 중요 도메인 로직들을 외부로 밀어낼때 사용해보면 좋을 것 같다.

// example 1

type Props = {
  onSumbit: (email, password) => void; // 해당 인터페이스를 맞춘 함수를 구현해서 내려주면 된다.
  buttons: ReactNode; // 해당 인터페이스를 맞춘 buttons 를 구현해서 내려주면 된다.
}

export function Form({ onSubmit }: Props) {
  const [email, onChangeEmail] = useInput('');
  const [password, onChangePassword] = useInput('');

  const handleSumbit = (event) => { 
    onSumbit(email, password); 
  }

  return <form onSumbit={handleSubmit}>
   <input type="email" value={email} onChange={onChangeEmail} />
   <input type="password" value={password} onChange={onChangePassword} />
   <div>
     {buttons}
   </div>
  </form>
}

export function LoginForm() {
  const handleLoginSumbit: Props['onSumbit'] = (email, password) => { //... }

  return <Form onSumbit={handleLoginSumbit} buttons={<button type="submit">로그인</button>}/>
}

export function SignUpForm() {
  const handleSignUpSumbit: Props['onSumbit'] = (email, password) => { //... }

  return <Form onSumbit={handleSignUpSumbit} buttons={<button type="submit">회원가입</button>}/>
}

// example2
type Props = { children: ReactNode; }

export function AppProvider({ children }: Props) {
  // client 라는 props 의 인터페이스를 맞춰서 내려주기만 하면 된다.
  // 보통 react 에서 의존성 주입(Dependency Injection)을 사용할 때 context-api 를 활용한다.
  return <QueryClientProvider client={new QueryClient()}>{children}</QueryClientProvider>
}

Feelings

항상 변경에 용이한 소프트웨어를 만들려면 어떻게 해야될지가 고민이다. 특히나 프론트엔드쪽은 변경이 너무 잦기 때문에 코드변경 또한 매우 잦다. 그래서 변경이 일어났을 때 유연하게 대처할 수 있는 방법이 없을까 고민하다가 oop 영역에서 사용하는 소프트웨어 설계원칙인 solid 에 관심을 가지게 되었다. 완벽하게 리액트 코드에 적용할순 없겠지만 단순히 설계 원칙이기에 최대한 적용할 수 있는 부분에는 적용해보면 좋지 않을까 싶다. 항상 코드를 짤 때 왜 이런 코드가 좀 더 나은 코드인지 생각하는 연습을 해보면 좋을 것 같다.

그리고 요즘들어 경력이 늘어날수록 내가 지금 막 커리어를 시작한 신입 개발자들과 다른게 무엇인지 고민하게 되는 것 같다. 지금 하는일에만 너무 안주하고 있다보니 이러다간 영원히 제자리 걸음일 것 같다는 생각이 든다. 이러한 고민들과 숙련도가 쌓여 다음단계로 가기 위한 발판이 되었으면 좋겠다.

Findings

변경에 용이한 소프트웨어를 만들기 위해 어떤 것들을 신경쓰면 좋은지에 대한 기초지식을 쌓았다. 오늘 정리한 것들을 실제 코드에 적용시켜보면서 실제로 어떤 점들이 나아졌는지에 대해서도 정리해보면 좋을 것 같다.

 

댓글을 작성해보세요.

채널톡 아이콘