==🛠️ 리액트 컴포넌트 설계와 SOLID 원칙==

1. 📜 SOLID 원칙 소개

  • ==SOLID==: 소프트웨어 설계의 5가지 원칙
  • 목표: 유지 보수 및 이해 용이성 증진

1.1. SOLID 구성 요소

  • SRP (단일 책임 원칙)
  • OCP (개방 폐쇄 원칙)
  • LSP (리스코프 치환 원칙)
  • ISP (인터페이스 분리 원칙)
  • DIP (의존성 역전 원칙)

2. 🎯 SRP (단일 책임 원칙)

  • 비즈니스 관점에서 요구사항을 분리
  • 변경 사항 발생 시 해당 책임 모듈만 수정

2.1. ❌ SRP 위반 사례: ==ActiveUsersList 컴포넌트==

  • 활성화된 유저 목록 렌더링
  • 서버에서 유저 데이터 fetching
  • 활성화된 유저 필터링

2.2. ✅ SRP 준수: 컴포넌트 분리

  • 커스텀 훅 (==useActiveUsers==)으로 데이터 fetching 로직 분리
  • 유틸 함수로 필터링 기능 분리
  • ==ActiveUsersList==: 데이터 수신 및 렌더링만 담당

3. 🚪 OCP (개방 폐쇄 원칙)

  • 확장에는 개방, 변경에는 폐쇄
  • 기존 코드 수정 없이 새로운 기능 추가

3.1. ❌ OCP 위반 사례: ==Header 컴포넌트==

  • ==pathname==에 따른 분기 처리로 기능 추가
  • 새로운 분기 추가 시 ==Header 컴포넌트 수정 필요==

3.2. ✅ OCP 준수: 컴포넌트 합성

  • ==children==을 통해 외부에서 기능 주입
  • ==Header 컴포넌트 수정 없이 확장 가능==

3.3. 카드 컴포넌트 예시

  • 이미지 모양, 설명 추가 등의 요구사항 발생
  • ❌ props 추가 방식은 분기 처리 증가로 유지 보수 어려움
  • ✅ 컴포넌트 분할 및 합성 (==CardThumbnail==, ==CardBody==, ==CardText==)
  • ==RountCardItem==, ==SquareCardItem 등 다양한 컴포넌트 생성==

4. 🔄 LSP (리스코프 치환 원칙)

  • 하위 타입은 상위 타입의 행위를 수행할 수 있어야 함
  • 상속 시 자식 객체가 부모 객체의 방향을 따라야 함
  • 리액트에서는 상속 대신 ==합성==을 권장하므로 적용 사례 드묾

4.1. 예시: ==fly() 기능==

  • 참새: LSP 적용 (상위 타입 ==fly() 기능 수행 가능)==
  • 펭귄: LSP 미적용 (상위 타입 ==fly() 기능 수행 불가)==

5. ✂️ ISP (인터페이스 분리 원칙)

  • 클라이언트가 사용하는 인터페이스만 제공
  • 불필요한 인터페이스 의존성 제거

5.1. ❌ ISP 위반 사례: ==Thumbnail 컴포넌트==

  • ==video 객체의 coverUrl==만 사용하지만, 인터페이스는 ==video 전체 객체를 받음==
  • 확장성 저하

5.2. ✅ ISP 준수: 인터페이스 수정

  • ==coverUrl 정보만 받도록 인터페이스 수정==
  • ==VideoList==에서 ==Video 썸네일과 LiveStream 썸네일 모두 정상적으로 표시==

6. 🔀 DIP (의존성 역전 원칙)

  • 고수준 모듈은 저수준 모듈의 구현에 의존 X
  • 추상화 레이어에 의존

6.1. ❌ DIP 위반 사례: ==ActiveUsersList 컴포넌트==

  • 컴포넌트 내부에 ==fetch 함수 직접 사용==
  • 렌더링 외 로직에 대한 관심사 불필요

6.2. ✅ DIP 준수: 추상화 레이어 추가

  • ==useActiveUsers 커스텀 훅을 통해 데이터 fetching 로직 추상화==
  • ==ActiveUsersList==는 추상화된 레이어에 의존

7. 📚 참고 자료

  • (참고 자료 링크)

  • SOLID 원칙은 소프트웨어 설계의 5가지 핵심 원칙으로, 코드의 유지보수성과 확장성을 극대화합니다.

  • 각 원칙(SRP, OCP, LSP, ISP, DIP)은 독립적으로 존재하면서도 상호 보완적으로 작용해 견고한 시스템을 만듭니다.

  • React 컴포넌트 설계에 SOLID 원칙을 적용하면 재사용성 높은 모듈을 만들고, 예상치 못한 변경으로부터 코드를 보호할 수 있습니다.

리액트 컴포넌트 설계 핸드북 SOLID 원칙의 모든 것

1. SOLID 원칙 왜 만들어졌나

소프트웨어 개발이 점점 복잡해지면서 코드는 거대해지고 얽히게 되기 마련이다. 처음에는 문제가 없어 보이지만, 작은 기능 하나를 추가하거나 버그를 수정할 때마다 전체 시스템이 흔들리는 경우가 다반사다. 이러한 문제들은 결국 개발 속도 저하, 높은 유지보수 비용, 그리고 개발자들의 고통으로 이어진다.

SOLID 원칙은 로버트 C. 마틴(일명 ‘Uncle Bob’)이 이러한 문제들을 해결하기 위해 제안한 객체 지향 설계의 핵심 5가지 원칙이다. 이 원칙들은 단순히 코드를 깔끔하게 만드는 것을 넘어, 소프트웨어의 확장성, 유연성, 재사용성, 그리고 유지보수성을 극대화하는 것을 목표로 한다. 즉, 코드가 미래의 변경에 능동적으로 대처할 수 있도록 하는 일종의 ‘설계 철학’이라고 할 수 있다.

React 컴포넌트도 예외는 아니다. 컴포넌트는 UI를 구성하는 독립적인 단위이지만, 잘못 설계될 경우 서로 강하게 결합되어 ‘스파게티 코드’가 될 수 있다. SOLID 원칙을 React 컴포넌트 설계에 적용하면, 컴포넌트들이 각자의 역할에 충실하고, 외부 변경에 영향을 덜 받으며, 필요에 따라 쉽게 확장할 수 있는 구조를 만들 수 있다. 이는 복잡한 UI를 효율적으로 관리하고, 대규모 애플리케이션을 안정적으로 구축하는 데 필수적이다.


2. SOLID 5가지 원칙 심층 분석

2.1. SRP (Single Responsibility Principle) 단일 책임 원칙

  • 정의: 하나의 클래스(혹은 모듈, 컴포넌트)는 단 한 가지의 책임만을 가져야 한다.

  • 핵심: 여기서 ‘책임’은 ‘기능’이 아니라 ‘변경의 이유(reason for change)‘를 의미한다. 즉, 특정 컴포넌트를 변경해야 하는 이유는 오직 하나여야 한다. 예를 들어, UserList 컴포넌트가 사용자 목록을 화면에 보여주는 기능과 서버에서 데이터를 가져오는 기능을 모두 수행한다면, 이 컴포넌트는 “데이터 구조 변경”과 “UI 변경”이라는 두 가지 변경의 이유를 갖게 된다.

  • React 컴포넌트에 적용:

    • 책임 분리: 데이터 패칭 로직, 상태 관리, 비즈니스 로직, UI 렌더링 로직을 분리한다. useStateuseEffect를 사용해 데이터를 가져오는 로직을 커스텀 훅으로 분리하거나, 데이터를 가공하는 로직을 유틸 함수로 분리하는 것이 좋은 방법이다.

    • 예시:

      • ActiveUsersList 컴포넌트가 데이터를 가져오고, 활성화된 사용자를 필터링하고, 목록을 렌더링하는 3가지 역할을 동시에 수행한다면 SRP 위반.

      • 개선:

        • 데이터 패칭: useUsers 커스텀 훅을 만들어 데이터 패칭과 에러 핸들링을 전담.

        • 데이터 필터링: filterActiveUsers 유틸 함수를 만들어 데이터 필터링을 전담.

        • UI 렌더링: ActiveUsersList는 이제 오직 필터링된 사용자 리스트를 props로 받아 화면에 보여주는 역할만 담당.

SRP 위반 컴포넌트SRP 적용 컴포넌트
UserList (데이터 패칭 + 필터링 + UI 렌더링)useUsers 커스텀 훅 (데이터 패칭)
filterActiveUsers 유틸 함수 (데이터 필터링)
ActiveUsersList 컴포넌트 (UI 렌더링)

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

  • 정의: 소프트웨어 구성 요소(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.

  • 핵심: 새로운 기능을 추가할 때 기존의 코드를 직접 수정하는 것이 아니라, 기존 코드를 그대로 둔 채 새로운 코드를 추가하는 방식으로 확장해야 한다는 원칙이다.

  • React 컴포넌트에 적용:

    • 컴포넌트 합성 (Composition): React의 핵심 철학 중 하나인 컴포넌트 합성은 OCP를 완벽하게 구현하는 방법이다. children props를 이용해 부모 컴포넌트의 내부 로직을 수정하지 않고도 다양한 형태의 자식 컴포넌트를 주입할 수 있다.

    • 예시:

      • Header 컴포넌트가 pathname에 따라 Home 또는 Dashboard UI를 직접 렌더링하는 분기 로직을 가지고 있다면 OCP 위반. 새로운 페이지(예: Settings)가 추가될 때마다 Header 컴포넌트의 코드를 직접 수정해야 하기 때문이다.

      • 개선:

        • Header 컴포넌트는 이제 children props를 받아서 렌더링하는 역할만 담당.

        • 사용 예:

          • <Header><HomeButton /></Header>

          • <Header><DashboardButton /></Header>

          • <Header><SettingsButton /></Header>

        • 이렇게 하면 Header 컴포넌트는 변경 없이 새로운 버튼 컴포넌트로 확장할 수 있다.


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

  • 정의: 하위 타입 객체는 상위 타입 객체가 가능한 행위를 수행할 수 있어야 한다.

  • 핵심: 부모 클래스가 사용되는 곳에 자식 클래스를 안전하게 대체할 수 있어야 한다는 원칙이다. 간단히 말해, 부모 객체의 기능을 오버라이드할 때, 부모의 행동 규약을 깨뜨리면 안 된다.

  • React 컴포넌트에 적용:

    • React는 클래스 기반의 상속보다는 **합성(Composition)**을 권장하기 때문에 LSP 원칙을 직접적으로 적용하는 경우는 드물다. 하지만 LSP의 정신은 여전히 중요하다.

    • 합성의 맥락에서 LSP의 정신: 특정 props를 받는 컴포넌트가 있다면, 그 컴포넌트를 대체하는 다른 컴포넌트도 동일한 props를 받았을 때 예상대로 동작해야 한다. 예를 들어, Card 컴포넌트가 data props를 받아서 렌더링한다면, ProductCardPostCard 같은 하위 컴포넌트들도 data props를 동일한 방식으로 처리해야 한다. 이는 컴포넌트의 일관성과 재사용성을 높인다.


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

  • 정의: 클라이언트는 자신이 사용하지 않는 인터페이스에 의존해서는 안 된다.

  • 핵심: 거대한 인터페이스 하나보다는 클라이언트에 특화된 여러 개의 작은 인터페이스를 만들어야 한다는 원칙이다. 불필요한 의존성을 줄여 코드의 응집도를 높이고 유연성을 확보한다.

  • React 컴포넌트에 적용:

    • 세밀한 props: 컴포넌트에 props를 넘겨줄 때, 필요한 데이터만 정확히 전달해야 한다. 불필요하게 거대한 객체(예: fullVideoObject)를 통째로 전달하기보다, 실제로 필요한 속성(예: coverUrl, title)만 넘겨주는 것이 좋다.

    • 예시:

      • VideoThumbnail 컴포넌트가 video 객체 전체를 props로 받는다면, video 객체의 구조가 변경되거나 다른 타입의 객체(예: LiveStream 객체)를 렌더링해야 할 때 문제가 발생할 수 있다. 이는 ISP 위반.

      • 개선:

        • VideoThumbnail 컴포넌트는 coverUrl props만 받도록 설계.

        • <VideoThumbnail coverUrl={video.coverUrl} />

        • <VideoThumbnail coverUrl={liveStream.thumbnailUrl} />

        • 이렇게 하면 VideoThumbnail 컴포넌트는 다양한 객체로부터 필요한 데이터만 받아 재사용될 수 있다.


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

  • 정의:

    1. 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.

    2. 추상화는 세부 사항에 의존해서는 안 된다. 세부 사항이 추상화에 의존해야 한다.

  • 핵심: 구체적인 구현(저수준 모듈)보다 추상화(고수준 모듈)에 의존해야 한다는 원칙이다. 이는 유연성과 재사용성을 극대화한다.

  • React 컴포넌트에 적용:

    • 데이터 주입 (Data Injection): 컴포넌트 내부에서 직접 데이터를 가져오는 대신, 상위 컴포넌트나 커스텀 훅을 통해 데이터를 주입받는 방식을 사용한다.

    • 예시:

      • ActiveUsersList 컴포넌트가 fetchUsers() 함수를 직접 호출하여 데이터를 가져온다면 DIP 위반. ActiveUsersList는 데이터 패칭이라는 ‘구체적인 구현(저수준 모듈)‘에 의존하게 된다.

      • 개선:

        • useActiveUsers 같은 커스텀 훅을 만들어 데이터 패칭 로직을 추상화.

        • ActiveUsersList는 이제 useActiveUsers 훅이 반환하는 users 데이터를 props로 받거나, 훅을 직접 호출하여 사용.

        • <UserListContainer users={activeUsers} />

        • UserListContaineruseActiveUsers 훅을 사용해 데이터를 가져오고, ActiveUsersListusers props를 전달한다. 이렇게 하면 ActiveUsersList 컴포넌트는 더 이상 데이터 패칭 방식에 의존하지 않고, 오직 사용자 목록을 렌더링하는 역할에만 집중할 수 있다. 이는 컴포넌트의 재사용성을 높이고 테스트를 용이하게 만든다.


3. 리액트 컴포넌트 설계와 SOLID 원칙의 결론

SOLID 원칙은 단순히 이론적인 개념이 아니다. React 컴포넌트 설계에 이 원칙들을 적용함으로써 우리는 더 유연하고, 확장 가능하며, 유지보수가 용이한 코드를 만들 수 있다.

  • SRP: 하나의 컴포넌트는 하나의 역할에 집중한다.

  • OCP: 컴포넌트 합성을 통해 기존 코드를 수정하지 않고도 기능을 확장한다.

  • LSP: props를 통해 컴포넌트 간의 예상 가능한 상호작용을 보장한다.

  • ISP: 컴포넌트가 필요로 하는 props만 정확하게 받도록 설계하여 불필요한 의존성을 제거한다.

  • DIP: 컴포넌트 내부에서 데이터를 직접 처리하지 않고, 추상화된 계층(커스텀 훅 등)을 통해 데이터를 주입받는다.

이러한 원칙들은 거대한 애플리케이션의 복잡성을 관리하고, 여러 개발자가 협업하는 환경에서 코드의 일관성을 유지하는 데 필수적인 지침이 된다. SOLID 원칙을 잘 이해하고 적용하는 것은 단순한 기술적 숙련을 넘어, 미래의 변경에 능동적으로 대처하는 견고한 소프트웨어 시스템을 구축하는 길이다.

대본

네 안녕하세요. 리액트 컴포넌트 설계와SOLID에 대해서 발표를 맡게 될 푸만능이라고 합니다진행 방식은 제가 공부한 SOLID에 대해서 알려주고이제 각 5가지 원칙을 리액트 컴포넌트에 어떻게 적용했는지말씀드리고 끝내도록 하겠습니다먼저 SOLID 원칙이란 소프트웨어 설계 5원칙으로소프트웨어 설계를 좀 더 유지 보수하기 쉽게그리고 이해하기 쉽게 해주는 5가지 원칙으로이 SOLID가 각각의 원칙의 앞글자를 따서 만들어진 것입니다이제 다섯 가지 원칙을 하나씩 살펴보도록 하겠습니다먼저 SRP 단일 책임 원칙에 대해서 말씀드리도록 하겠습니다SRP 단일 책임 원칙 단어만 들으면단일한 동작만 가지고 분리한다는 개념을 가지기 쉬운데요사실은 SRP는 소프트웨어 공학적인 측면으로 생각을 해보면사용자나 이해관계자 등의 변경을요청하는 사람들을 중심으로책임을 분리하는 원칙입니다즉, SRP 원칙은 비즈니스 관점에서 책임즉 요구사항을 분리하는 원칙으로 볼 수 있습니다그리고 분리된 모듈은 한 가지 책임에 관한변경사항이 생겼을 때만 코드를 수정하게 되는 구조가좋은 구조라고 하고 있습니다이제 자세한 내용은코드를 통해서 말씀드리도록 하겠습니다위의 코드는 SRP가 위반된 코드라고 볼 수 있는데요왜냐하면 컴포넌트 네이밍에서 볼 수 있듯이해당 컴포넌트는 활성화된 유저들의 목록들을렌더링하는 기능을 수행하고요뿐만 아니라 서버에서 유저 데이터를 가져오는 기능그리고 유저 데이터 정보 중활성화된 유저를 필터링하는 기능을한 컴포넌트 내에서 수행하고 있으므로SRP가 위반됐다고 볼 수 있습니다따라서 먼저 이제 useState와 useEffect를 사용하여데이터 fetching하는 로직을커스텀 훅으로 분리를 해보았습니다다음은 이제 필터링하는 기능을유틸함수로 분리를 해보았습니다지금 보시는 ActiveUsersList 컴포넌트는지금 보시면 분리가 굉장히 잘 됐다고 볼 수 있는데요사실은 제가 생각하는 ActiveUsersList의 요구사항은활성화된 유저 리스트에 대한 데이터를 받기만 하고그거를 렌더링하는 기능을 수행하기를 원하고 있어서useActiveUsers라는 커스텀 훅을 또 분리를 해줬습니다그래서 사용자 데이터를 서버에서 가져오고필터링 로직을 한 번에 처리하기 위한커스텀 훅을 만들고 분리를 했습니다이렇게 단일 모듈로 분리를 함으로써좀 더 유지 보수하기 좋게 할 수 있습니다다음은 OCP, 개방 폐쇄 원칙에 대해서 말씀드리도록 하겠습니다OCP는 소프트웨어 구성요소에서 확장에는 열려 있어야 하고변경에는 닫혀 있어야 한다는 규칙인데요쉽게 말해서 기존의 코드는 유지하고새로운 코드로 확장을 하는 구조라고 생각하시면 될 것 같습니다코드를 통해서 좀 더 확인해 보도록 하겠습니다지금 Header 컴포넌트는 pathname의 분기절에 따라서홈으로 갈 때와 그리고 대시보드를 갈 때에이제 기능이 추가되는 모습을 볼 수 있습니다만약에 홈과 대시보드 말고또 다른 분기절을 통해서이제 또 다른 컴포넌트를 기능을 더 추가하고 싶으면Header 컴포넌트를 직접 변경해야 되는 상황이 발생하게 됩니다이는 OCP를 위반했다고 볼 수 있습니다그렇다면 이를 어떻게 해결할 수 있을까요?컴포넌트 합성을 통해서 해결할 수 있는데요위에 보시면 앞서 Header 내부의 pathname을 통해서분기 처리했던 로직을 children을 통해서외부로 주입하면서 기존의 Header 컴포넌트는변경사항을 적용하지 않고 확장에는 용이하게확장에서는 좀 더 용이한 구조로 변경을 하였습니다이는 OCP를 잘 지킨 코드라고 할 수 있습니다다음은 실제 프로젝트에서어떻게 쓰일까 고민을 해보았는데요카드 컴포넌트를 예제로 들어서좀 더 설명을 해보도록 하겠습니다카드 컴포넌트는 기존의 UI로만 기능을 수행한다면지금 같은 코드로도 충분히 잘 사용할 수 있을 것 같은데요만약에 카드 컴포넌트에 밑에 그린 것 같이이미지가 동그랗게 된다거나 자세히 보일지 모르겠는데디스크립션 설명이 추가된다는 변경 사항이 요구된다면어떻게 변경 사항에 대처할 수 있을까요?첫 번째는 props를 추가하는 방법입니다변경 사항에 따라서 props를 계속 추가함에 따라서구현 구절에서는 이런 분기처리 로직들이 점점 늘어나게 되고그럴 경우 유지 보수하기 굉장히 힘들 것으로 보입니다그래서 CardItem에 앞서 말씀드렸다시피CardItem의 요구사항이 늘어났기 때문에요구사항에 따라서 책임을 분리하는 방법을채택을 한번 해보도록 하겠습니다앞서 이미지와 그리고 디스크립션 관련해서 요구사항이 늘어났는데요그에 따라서 저는 카드 아이템을 컴포넌트를좀 더 잘게 나누어서 다음과 같이CardThumbnail, CardBody 그리고 CardText세 가지의 컴포넌트를 좀 더 잘게 쪼개 보았습니다그리고 이를 잘게 나눈 컴포넌트를 조합하고 합성하면서RountCardItem 그리고 SquareCardItem 등다양한 카드 컴포넌트를 만들 수 있습니다다음은 LSP 원칙에 대해서 설명을 해보도록 하겠습니다하위 타입 객체는 상위 타입 객체에서가능한 행위를 수행할 수 있어야 한다고설명을 하고 있습니다그림을 통해서 설명하도록 하겠습니다객체 구조화에 대한 그림인데요앞서 상위 타입 객체의 fly() 기능을하위 타입 객체인 펭귄도 fly()를 적용해야 한다는 이야기인데요따라서 참새 같은 경우에는LCP를 적용이 되었다고 볼 수 있고반대로 펭귄은 LCP가 적용이 되지 않았다고 볼 수 있습니다리스코프 치환 원칙은 올바른 상속을 위해자식 객체의 확장이 부모 객체의 방향을온전히 따르도록 권고하는 원칙입니다하지만 리액트에서는 이제 상속 대신 합성을 사용하여컴포넌트 간의 코드를 재사용한다고제안을 하고 있기 때문에LSP를 적용하는 일은 극히 드물 것으로 보입니다다음은 ISP 원칙에 대해서 말씀드리도록 하겠습니다클라이언트가 실제로 사용하는 인터페이스를 만들어야 한다는 의미로인터페이스를 사용에 맞게끔각기 분리해야 한다는 설계 원칙입니다예시 코드를 한번 살펴보도록 하겠습니다Thumbnail 컴포넌트의 구현부를 보시면video 객체의 corverUrl만 사용하고 있음에도 불구하고인터페이스에서는 비디오 전체를 가져오는 구조를 띄고 있는데요이렇게 활용할 경우에는 구현에는 이상이 없지만확장에는 용이하지 못하다는 단점이 있습니다예시를 통해서 좀 더 자세히 보도록 하겠습니다이 VideoList 같은 경우에는Video에 대한 내용과 LiveStream이라는 내용을Thumbnail 카드를 보여주는 리스트인데요만약에 인터페이스를 지키지 못할 경우에기존에 Video에서는 잘 보여지던 게LiveStream을 적용시킬 시에제대로 보여줄 수 없는 상황에 놓여지게 됩니다만약에 ISP를 잘 적용하면 어떻게 될까요?기존에 video에 있는 coverUrl에 있는 데이터를 사용했으므로이제 인터페이스도 coverUrl에 있는 정보만 넣을 수 있도록 수정하였습니다따라서 VideoList는 Video 썸네일과LiveStream 썸네일을 둘 다 보여줄 수 있어서확장에 좀 더 용이한 구조로 만들 수 있게 되었습니다다음은 DIP에 대해서 설명드리도록 하겠습니다고수준 모듈이 저수준 모듈의 구현에의존해서는 안된다는 원칙인데요그림을 통해서 좀 더 자세히 설명하도록 하겠습니다우리가 전기를 사용하기 위해서는 플러그를 끼우면 되지우리가 직접 배선을 사용해서전기를 사용하지 않아도 된다는 내용인데요쉽게 말해서 저희가 컴포넌트를 설계를 할 때컴포넌트에 있는 데이터를 가져와서렌더링 하는 거에만 신경을 쓰면 되지다른 로직에 대해서는 신경 쓰지 말자추상화 레이어를 의존해서 사용하자라는 법칙입니다코드를 통해서 좀 더 자세히 살펴보도록 하겠습니다ActiveUsersList라는 컴포넌트에는 이제 fetch 함수가직접 들어가 있는 걸 볼 수 있습니다이는 사실 아까 말씀드렸던 것처럼아까 ActiveUsersList는 데이터를 받아서렌더링하는 기능에만 초점이 맞춰져 있지fetch에 대한 로직에 대해서는 전혀 관심사가 없습니다따라서 DIP 원칙이 위반됐다고 볼 수 있는데요따라서 이제 추상화 계층의 유저라는커스텀 훅을 통해서 레이어 계층을 두어서DIP를 적용한 모습을 볼 수 있습니다네 이상 참고 자료고요마치도록 하겠습니다 감사합니다