React에 적용한 SOLID 원칙
1. 단일책임원칙 Single Responsibility Principle
- 함수나 클래스는 한가지 일만 수행해야 한다. 이는 리액트의 컴포넌트도 마찬가지. 컴포넌트 하나가 수행하는 일은 하나인 것이 이상적.
import React, {useEffect, useReducer, useState} from "react";
const initialState = {
isLoading: true
};
function reducer(state, action) {
switch (action.type) {
case 'LOADING' :
return {isLoading: true}
case 'FINISHED' :
return {isLoading: false}
default:
return state;
}
}
export const SingleResponsibilityPrinciple = () => {
const [users, setUsers] = useState([])
const [filteredUsers, setFilteredUsers] = useState([])
const [state, dispatch] = useReducer(reducer, initialState);
const showDetails = (userId) => {
const user = filteredUsers.find(user => user.id === userId)
alert(user.contact)
}
useEffect(() => {
dispatch({type: 'LOADING'})
fetch('https://jsonplaceholder.typicode.com/users')
.then(response => response.json())
.then(json => {
dispatch({type: 'FINISHED'})
setUsers(json)
})
}, [])
useEffect(() => {
const filteredUsers = users.map(user => {
return {
id: user.id,
name: user.name,
contact: `${user.phone}. ${user.email}`
};
});
setFilteredUsers(filteredUsers)
}, [users])
return (
<>
<div> Users List <div>
<div> Loading state: {state.isLoading ? 'Loading' : 'Success'} <div>
{users.map(user => {
return (
<div key={user.id} onClick={() => showDetails(user.id)}>
<div> {user.name} <div>
<div> {user.email} <div>
<div>
)
})}
<>
)
}
- 이 컴포넌트는 하는 일이 매우 많다.
1. 원격 데이터 가져오기
2. 데이터 필터링하기
3. 복잡한 상태를 관리하기
4. 복잡한 UI 기능
- 하나의 컴포넌트에서 너무 많은 일을 하는 것은 좋은 코드가 아니다.
- 각각을 분리해서 각 파일의 양을 줄이고 재사용할 수 있도록 분리해 주는 것이 단일 책임 원칙을 따르는 것이다.1
2. 개방 폐쇄 원칙 Open Closed Principle
- 소프트웨어 엔티티는 확장을위해 열려 있어야하지만, 수정을 위해 닫혀야 한다. 즉, 소스코드를 수정하지 않고 확장 가능
* 리액트에서는 어떻게 사용?
import React, { FC } from "react";
interface InputProps {
value: string;
onChange: React.ChangeEventHandler<HTMLInputElement>;
className?: string;
label: string;
}
const Input: FC<InputPrps> = ({label, ...props}) => {
return (
<div>
<label> {label} <label>
<input {...props} />
<div>
)
}
export default Input
- 위과 같은 컴포넌트는 개발을 하다보면 props가 늘어날 수 있다.
import React, { FC } from "react";
interface InputProps {
inputRef: React.RefObject<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>;
className?: string;
containerClassName?: string;
formGroupClassName?: string;
type?: "text" | "search" | "password" | "email" | "textarea" | "select";
textareaRows?: number;
name?: string;
value: string;
isTouched: boolean;
onChange: React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement | HTMLAreaElement>;
onInput: React.FormEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onBlur: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>;
onInvalid: React.FormEventHandler<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>;
placeholder?:
id: string;
maxLength?: number;
hasClearButton?: boolean;
addonLeft?: React.ReactNode;
addonRight? React.ReactNode;
addonLeftClassName?: string;
addonRightClassName?: string;
helpText?: React.ReactNode;
isShortInput?: boolean;
isLoading?: boolean;
}
const Input: FC<InputProps> = ({
className,
containerClassName,
formGroupClassName,
type = "text",
textareaRows,
name,
value = "",
isTouched,
onChange,
onInput,
onClearClick,
onBlur,
onInvalid,
placeholder,
id,
maxLength,
hasClearButton,
label,
labelStyle,
addonLeft,
addonRight,
addonLeftClassName,
addonRightClassName,
helpText,
isShortInput,
isLoading,
...validationProps
}) => {
return (
<div>
<input />
(...)
<div>
)
}
- (물론, props를 이렇게 많이 넘겨줘야 한다면 잘못 짠 코드이다, 예시용)
- 확장 가능하지만 수정하지 않는 컴포넌트를 만드는 방법은 다음과 같다,
<InputContainer ...props>
<InputLabel ...props />
<InputLeftAddon ...props />
<InputControl ...props />
<InputRightAddon ...props />
<InputContainer>
3. Liskov 대체원리 Liskov Principle
- shape 라는 슈퍼클래스가 있고 이로부터 파생된 서브클래스 T 와 S 가 있다는 가정,
- Liskov 대체원리란, T와 S가 같은 슈퍼클래스를 공유하는한, T 와 S는 서로 교체할 수 있어야 한다는 것
class Shape {
render() {
throw new Error("Cannot render 'Shape'");
}
}
class Square extends Shape {
constructor (height, width) {
this.height = height;
this.width = width;
}
render() {
Canvas2d
.drawRect(0, 0, height, width)
.fill("white")
.border("1px", "black");
console.log(`Rendering Square (0, 0, ${height}, ${width})`)
}
}
class Circle extends Shape {
constructor(height, width) {
this.height = height;
this.width = width;
}
render() {
Canvas2d
.drawCircle(0, 0, height, width)
.fill("white")
.border("1px", "black");
console.log(`Rendering Circle (0, 0, ${heigh}, ${width})`)
}
}
class ShapeRenderer {
constructor(shape) {
this.shape = shape;
}
setShape(shape) {
this.shape = shape;
}
render() {
this.shape.render();
}
}
const mySquare = new ShapeRender(mySquare);
myRenderer.render();
myRenderer.setShape(circle);
myRenderer.render();
- 슈퍼클래스 Shape로부터 Square 와 Circle 을 만들고, Renderer 에서 인스턴스를 교체함.
- 둘 다 같은 슈퍼클래스로부터 파생됐기 때문.
4. 인터페이스 분리원칙
- 필요하지않은 것에 의존하면 안된다 즉, 자신이 사용하지 않는 기능(인터페이스)에는 영향을 받지않아야함.
* shape 기능을 유지하면서 하위타입으로 확장할 수 잇기 때문에 Liskov 원칙의 예
interface Shape {
render(): void;
area(): number;
radius(): number;
}
- 공통적으로 사용할 수 있는 객체 Shape 가 있고 이 객체 안의 프로퍼티들을 예제처럼 작성했다고 가정, 만약에 사각형이나 삼각형 객체가 필요하다면 반지름 값인 radius가 없으므로 인터페이스를 분리할 필요가 있다.
interface Shape {
render(): void;
area(): number;
}
interface Circle extends Shape {
radius(): number;
}
- 이렇게 인터페이스를 분리하면 사각형객체에는 필요없는 radius값을 필수적으로 구현하지않게됨.
5. 의존성 역전 원칙 Dependency Inversion Principle
- 고수준모듈이 저수준모듈의 구현에 의존해서는 안된다.
- 즉, 어플리케이션이 특정한 함수나 인스턴스가 아닌 인터페이스, 또는 추상화에 의존해야한다.
- 하지만 리액트에서는 n개의 component를 가지고 하나의 결과를 내기때문에 쉽지않음
- 의존성반전을 리액트에서 적용한다면,
const Foo = ({ someVal }) => {
return (
<div>
{someFilterFn(someval)}
<div>
)
}
- someFilterFn()함수는 외부의 함수, 클래스 혹은 모듈에서 받았다고 가정
- 그렇다면, 자식 컴포넌트는 부모로부터 받은 someVal 과 외부로 받아온 someFilterFn 두가지이다.
- 이 경우, filtering 함수를 부모로부터 받아와 자식컴포넌트의 종속성을 줄일 수 있다. 이를 수정하려면,
const Foo = ({ callback, someVal }) => {
return (
<div>
{callback(someval)}
<div>
)
}
- 필터를 수행하는 로직이 상위 컴포넌트 내에 캡슐화 되어 있기 때문에 구성요소에 대한 테스트가 단순화된다.