React에 적용한 SOLID원칙

the Other Object·2023년 4월 17일
0

React에 적용한 SOLID 원칙

1. 단일책임원칙 Single Responsibility Principle

  • 함수나 클래스는 한가지 일만 수행해야 한다. 이는 리액트의 컴포넌트도 마찬가지. 컴포넌트 하나가 수행하는 일은 하나인 것이 이상적.
import React, {useEffect, useReducer, useState} from "react";


const initialState = {
  isLoading: true
};

//COMPLEX STATE MANAGEMENT
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)
    // 배열의 특정 값 찾기 : find(), filter()
    // 1) find() : `arr.find(callback(element[, index[,array]])[, thisArg])`
    //			   배열에서 특정 값을 찾는 조건을 콜백함수를 통해 전달하여, 조건에 맞는 값 중 첫번째 값을 리턴한다. 만약, 배열에 조건을 만족하는 값이 없으면 undefined를 리턴.
    //			   - 파라미터 callback(element, index, array)함수 : 조건을 비교할 callback 함수이고, 3개의 파라미터가 전달 된다. 콜백함수에서 사용자가 테스트할 조건을 정의하고, 만약 배열의 값이 조건에 부합하여 true를 리턴하면, 해당 배열의 값이 find() 함수의 리턴값이 됨. 조건에 부합하는 값을 찾으면 그 이휴의 배열값은 테스트되지 않는다. (element: 현재 처리 중인 배열의 element, index: 현재 처리 중인 배열의 index(optional), array: find()가 호출된 배열(optional))
    //			   - thisArg(optional) : callback을 실행할 때 this로 사용할 객체
    //			   - 리턴값 : callback함수에 정의한 조건에 부합하는 배열의 첫번째 값을 리턴한다. 조건에 부합하는 배열값이 없을 경우 undefined 리턴. (**참조: callback함수의 조건에 부합하는 배열의 첫번째 index값을 알아내기 위해서는 findIndex()함수 사용한다.)
    // const arr = [{naem: 'apple', price: 1000}, {name: 'lemon', price: 2000}, {name: 'melon', price: 3000}];
    // const isApple = (element) => { if(element.name === 'apple') { return (true;)}}    ▶︎2. isApple()함수는 파라미터로 입력받은 객체(element)의 name이 'apple'이면 true를 리턴한다.
    // const apple = arr.find(isApple);	  ▶︎1. find()함수에 isApple()이라는 callback함수를 전달했다. ▶︎3. find()함수는 파라미터로 전달된 callback함수가 true를 리턴하면 해당 배열의 값을 리턴하고, 더이상 나머지 배열의 값은 callback함수로 전달하지 않는다.
    // console.log(apple.name); //apple   ▶︎4. 결국 find()함수는 callback함수에 정의 된 조건에 부합하는 배열의 첫번째 값을 리턴하는 것이고, 해당 예제에서 find()함수는 arr배열의 여러 객체 중, name === 'apple'인 arr[0]의 값을 리턴했음.
    // console.log(apple.price); //1000
    
    
    // 2) filter() : find()는 특정조건에 부합하는 배열의 첫번째값만 리턴인 반면, filter()는 모든 값을 배열형태로 리턴한다.
    // const arr = [{naem: 'apple', price: 1000}, {name: 'lemon', price: 2000}, {name: 'apple', price: 3000}];
    // const apples = arr.filter(isApple);   ▶︎1. filter()함수는 arr배열에서 object의 name === 'apple'인 모든 객체를 찾아서 새로운 배열로 생성하여 리턴한다.
    // console.log(apples.length);  //2    ▶︎2. filter()함수가 리턴하는 apples는 length가 2인 배열이고, 배열의 2개의 값은 arr[0], arr[2]의 값과 같다. 
    // console.log(apples[0].name + ',' + apples[0].price)  //apple, 1000
    // console.log(apples[1].name + ',' + apples[1].price)  //apple, 3000
    alert(user.contact)
  }
  
  //REMOTE DATA FETCHING
  useEffect(() => {
    dispatch({type: 'LOADING'})
    fetch('https://jsonplaceholder.typicode.com/users')
    	.then(response => response.json())
    	.then(json => {
    		dispatch({type: 'FINISHED'})
      		setUsers(json)
    	})
  }, [])
  
  //PROCESSING DATA
  useEffect(() => {
    const filteredUsers = users.map(user => {
    	return {
          id: user.id,
          name: user.name,
          contact: `${user.phone}. ${user.email}`
        };
    });
    setFilteredUsers(filteredUsers)
  }, [users])
  
  // COMPLEX UI RENDERING
  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() {
    //psuedocode
    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() {
    //psuedocode
    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();
  }
}
//Create our instances of subtype 'Shape'
const mySquare = new ShapeRender(mySquare);
myRenderer.render();
myRenderer.setShape(circle);
myRenderer.render();
		- 슈퍼클래스 Shape로부터 Square 와 Circle 을 만들고, Renderer 에서 인스턴스를 교체함.
        - 둘 다 같은 슈퍼클래스로부터 파생됐기 때문.

4. 인터페이스 분리원칙

  • 필요하지않은 것에 의존하면 안된다 즉, 자신이 사용하지 않는 기능(인터페이스)에는 영향을 받지않아야함.
* shape 기능을 유지하면서 하위타입으로 확장할 수 잇기 때문에 Liskov 원칙의 예

//General purpose interface
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>
  )
}
		- 필터를 수행하는 로직이 상위 컴포넌트 내에 캡슐화 되어 있기 때문에 구성요소에 대한 테스트가 단순화된다.

0개의 댓글