원문 : https://konstantinlebedev.com/solid-in-react/
- 단일 책임 원칙(SRP)
- 개방 폐쇄 원칙(OCP)
- Liskov 치환 원칙(LSP)
- 인터페이스 분리 원칙(ISP)
- 의존성 역전 원칙(DIP)
Single responsibility principle (SRP)
- 너무 많은 작업을 수행하는 큰 컴포넌트를 더 작은 컴포넌트로 나눕니다.
- 주요 컴포넌트 기능과 관련 없는 코드를 별도의 유틸리티 함수로 추출
- 연결된 기능을 커스텀 훅으로 캡슐화
이제 이 원칙을 적용하는 방법을 살펴보겠습니다.
활성 사용자 목록을 표시하는 다음 예제 컴포넌트를 고려하여 시작하겠습니다.
const ActiveUsersList = () => {
const [users, setUsers] = useState([])
useEffect(() => {
const loadUsers = async () => {
const response = await fetch('/some-api')
const data = await response.json()
setUsers(data)
}
loadUsers()
}, [])
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
return (
<ul>
{users.filter(user => !user.isBanned && user.lastActivityAt >= weekAgo).map(user =>
<li key={user.id}>
<img src={user.avatarUrl} />
<p>{user.fullName}</p>
<small>{user.role}</small>
</li>
)}
</ul>
)
}
이 컴포넌트는 현재 비교적 코드가 짧지만
데이터를 가져오고, 필터링하고, 컴포넌트 자체와 개별 리스트 아이템을 렌더링하는 등 이미 몇 가지 작업을 수행하고 있습니다.
어떻게 분해할 수 있는지 봅시다.
우선 useState 와 useEffect 훅은 커스텀 훅으로 추출할 수 있는 좋은 기회입니다.
const useUsers = () => {
const [users, setUsers] = useState([])
useEffect(() => {
const loadUsers = async () => {
const response = await fetch('/some-api')
const data = await response.json()
setUsers(data)
}
loadUsers()
}, [])
return { users }
}
const ActiveUsersList = () => {
const { users } = useUsers()
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
return (
<ul>
{users.filter(user => !user.isBanned && user.lastActivityAt >= weekAgo).map(user =>
<li key={user.id}>
<img src={user.avatarUrl} />
<p>{user.fullName}</p>
<small>{user.role}</small>
</li>
)}
</ul>
)
}
다음으로 컴포넌트가 렌더링하는 JSX를 살펴보겠습니다.
const UserItem = ({ user }) => {
return (
<li>
<img src={user.avatarUrl} />
<p>{user.fullName}</p>
<small>{user.role}</small>
</li>
)
}
const ActiveUsersList = () => {
const { users } = useUsers()
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
return (
<ul>
{users.filter(user => !user.isBanned && user.lastActivityAt >= weekAgo).map(user =>
<UserItem key={user.id} user={user} />
)}
</ul>
)
}
마지막으로 API에서 얻은 모든 사용자 목록에서 비활성 사용자를 필터링하는 논리가 있습니다.
const getOnlyActive = (users) => {
const weekAgo = new Date()
weekAgo.setDate(weekAgo.getDate() - 7)
return users.filter(user => !user.isBanned && user.lastActivityAt >= weekAgo)
}
const ActiveUsersList = () => {
const { users } = useUsers()
return (
<ul>
{getOnlyActive(users).map(user =>
<UserItem key={user.id} user={user} />
)}
</ul>
)
}
const useActiveUsers = () => {
const { users } = useUsers()
const activeUsers = useMemo(() => {
return getOnlyActive(users)
}, [users])
return { activeUsers }
}
const ActiveUsersList = () => {
const { activeUsers } = useActiveUsers()
return (
<ul>
{activeUsers.map(user =>
<UserItem key={user.id} user={user} />
)}
</ul>
)
}
Open-closed principle (OCP)
const Header = () => {
const { pathname } = useRouter()
return (
<header>
<Logo />
<Actions>
{pathname === '/dashboard' && <Link to="/events/new">Create event</Link>}
{pathname === '/' && <Link to="/dashboard">Go to dashboard</Link>}
</Actions>
</header>
)
}
const HomePage = () => (
<>
<Header />
<OtherHomeStuff />
</>
)
const DashboardPage = () => (
<>
<Header />
<OtherDashboardStuff />
</>
)
const Header = ({ children }) => (
<header>
<Logo />
<Actions>
{children}
</Actions>
</header>
)
const HomePage = () => (
<>
<Header>
<Link to="/dashboard">Go to dashboard</Link>
</Header>
<OtherHomeStuff />
</>
)
const DashboardPage = () => (
<>
<Header>
<Link to="/events/new">Create event</Link>
</Header>
<OtherDashboardStuff />
</>
)
Liskov substitution principle (LSP)
Interface segregation principle (ISP)
type Video = {
title: string
duration: number
coverUrl: string
}
type Props = {
items: Array<Video>
}
const VideoList = ({ items }) => {
return (
<ul>
{items.map(item =>
<Thumbnail
key={item.title}
video={item}
/>
)}
</ul>
)
}
type Props = {
video: Video
}
const Thumbnail = ({ video }: Props) => {
return <img src={video.coverUrl} />
}
type LiveStream = {
name: string
previewUrl: string
}
type Props = {
items: Array<Video | LiveStream>
}
const VideoList = ({ items }) => {
return (
<ul>
{items.map(item => {
if ('coverUrl' in item) {
// it's a video
return <Thumbnail video={item} />
} else {
// it's a live stream, but what can we do with it?
}
})}
</ul>
)
}
보시다시피 여기에 문제가 있습니다.
type Props = {
coverUrl: string
}
const Thumbnail = ({ coverUrl }: Props) => {
return <img src={coverUrl} />
}
이 변경으로 이제 비디오와 라이브 스트림의 썸네일을 렌더링하는 데 사용할 수 있습니다.
type Props = {
items: Array<Video | LiveStream>
}
const VideoList = ({ items }) => {
return (
<ul>
{items.map(item => {
if ('coverUrl' in item) {
// it's a video
return <Thumbnail coverUrl={item.coverUrl} />
} else {
// it's a live stream
return <Thumbnail coverUrl={item.previewUrl} />
}
})}
</ul>
)
}
Dependency inversion principle (DIP)
import api from '~/common/api'
const LoginForm = () => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (evt) => {
evt.preventDefault()
await api.login(email, password)
}
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Log in</button>
</form>
)
}
type Props = {
onSubmit: (email: string, password: string) => Promise<void>
}
const LoginForm = ({ onSubmit }: Props) => {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (evt) => {
evt.preventDefault()
await onSubmit(email, password)
}
return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">Log in</button>
</form>
)
}
import api from '~/common/api'
const ConnectedLoginForm = () => {
const handleSubmit = async (email, password) => {
await api.login(email, password)
}
return (
<LoginForm onSubmit={handleSubmit} />
)
}
과거에는 "멍청한" 프레젠테이션 컴포넌트를 생성한 다음
주석
왜 Container Component일까?를 생각해보면
리액트 개발자는 스프링의 IOC 컨테이너 역할을 하는 컴포넌트를 직접 개발한다는 점이네요.
즉 의존성 역전 원칙을 적용한 퓨어 View Asset Component와 훅, 서버 API 데이터를 Connect하는 Container를 개발하는거죠.
반대로 스프링 개발자들은 Container Component의 존재를 스프링 IOC 컨테이너 덕택에 숨길 수 있는거구요
스프링 IOC 컨테이너와 Context API는 암시적인 측면에서 유사하네요!
반대로 Container Component는 개발자의 공수가 좀 더 들어가는 대신 명시적이라는 장점이 있겠습니다.
(스프링 없으면 의존성 주입 못하는 스프링 개발자랑 다르게 컨테이너 컴포넌트 패턴은 프레임워크 중립적인건 있겠죠)
좀 더 코드 레벨에서 개발자의 컨트롤이 들어간다고 할 수 있겠네요.
최근에 개발바닥 영상을 보고 제 블로그에 방문하시는 분들이 많은것 같은데...
개인적으로 두분 다 시니어 엔지니어로서 좋지 않은 태도를 영상에서 자꾸 보여주고 있다고 생각은 듭니다.
할말하않입니다.
'FrontEnd' 카테고리의 다른 글
언제 리코일을 사용하는게 좋을까? (6) | 2022.07.17 |
---|---|
리액트 훅의 클로저 트랩(closure trap) 이해하기 (0) | 2022.07.17 |
리액트 쿼리 : FAQ(자주 묻는 질문) (2) | 2022.07.17 |
프레이머 모션[framer motion] 기초 2부 : 레이아웃 애니메이션 (0) | 2022.07.16 |
프레이머 모션[Framer Motion] 기초 1편 : 생기초 알아보기 (0) | 2022.07.15 |