LeChuck

원티드 FE 프리온보딩 회고 (2)

·24 min to read

1. 캐싱

빵과 넷플릭스로 알아보는 캐싱

웹 서비스 캐시 똑똑하게 다루기

기업과제 요구사항 중에 'React-Query와 같은 라이브러리 사용 없이 API 호출별로 로컬 캐싱을 구현하라'는 항목이 있었다. 검색어 입력시 API 캐싱 여부에 따라 캐싱 된 결과값을 사용하거나 API를 호출해오는 로직을 cacheStoarge로 구현했는데, 이 과정에서 알게 된 것들을 기록한다.

캐시는 하드웨어단에서의 캐싱부터 서버, DB 캐싱 등등 수많은 방식이 있겠지만 여기서는 임의로 그 범위를 좁히고 서버 경유 없이 클라이언트에서 단독으로 캐시하는 방법에 대해 다룬다.

Client-side caching

a) In-memory caching

Inside React Query

React-Query는 기본적으로 In-memeory 캐싱 방식을 지원한다. ‘이딴 게 캐싱이냐’, ‘React-Query는 마케터에게 상을 줘야 한다’라며 치를떠는 개발자를 본 적이 있는데, 아마도 그 분이 분노한 포인트는 in-memory caching과 persist caching의 차이점을 모르거나 오인한 채 사용하는 개발자들을 향한 것이 아니었을까 싶다.

  • QueryClient라는 인스턴스가 있고, 이는 React Context API를 통해 전역으로 관리된다.
  • QueryClientQueryCacheMutationCache를 담는 용기다(a vessel that holds the cache)
  • QueryCache 는 in-memory object로, QueryKey / Query (class) kev-value Pair다
  • useQuery를 호출하면 Observer가 생성되어 QueryCache 내부의 Query를 가리킨다. (캐싱)
  • persistsQueryClient를 활용하면 localStorage에 캐싱할 수 있다.

image

React-Query 상세 동작

image

  • QueryCache 내부의 Query로부터 대부분의 React-query 로직이 수행된다.
  • Query에는 Query의 모든 데이터가 포함되어 있을 뿐 아니라 retry, cancellation, de-deuplication 로직이 내장되어 있다.
  • 가장 중요한 점은 QueryQueryObserver를 통해 Query data에 관심있어하는 대상을 파악할 수 있고, 또한 모든 변경사항을 QueryObserver에게 알릴 수 있다는 점이다.

image

  • ObserveruseQuery 호출시에 생성되어 하나의 Query 를 카리킨다. 이것이 useQuery호출 시 queryKey를 지정하는 이유다.

image

  • Observer가 없는 Query를 inactive Query라고 부른다. inactive Query는 React Query Devtools에 회색으로 표시된다.
  • inactive Query는 여전히 캐시에 있지만 어떤 컴포넌트에서도 사용중이지 않은 Query를 의미한다.
  • 좌측의 숫자는 활성화되어 있는 Observer의 수를 나타낸다.

image

image

  • 컴포넌트 관점에서의 흐름은 아래와 같다.
    1. 컴포넌트가 mount되고 useQuery를 호출한다. 이 때 Observer가 생성되어 QueryCache 내부의 Query를 구독한다.
    2. 기존에 Query가 없었다면 이때 첫 생성 후 구독을 맺는 것이고, Query가 존재했고 stale한 상황이면 refetch 수행.
    3. useQuery 호출 과정은 아직 끝나지 않았다. fetch가 진행되면 Query의 상태가 변경되고 Observer는 이를 구독중인 컴포넌트(useQuery를 호출한)에게 알려서 컴포넌트가 리렌더링 할 수 있도록 한다.

b) Persist caching (with web storage)

웹용 스토리지

서비스 워커 캐싱 및 HTTP 캐싱

persist caching이라는 명칭은 in-memory와 대조되는 캐싱 방식으로 구분짓기 위해 임의로 명명했다. 새로고침하면 휘발되는 in-memory 방식의 캐싱과는 달리, 저장소를 별도로 두기 때문에 장기적인 보관이 가능하다. Web stoarge 선택 가이드는 이 링크에 잘 제시되어 있다. 일반적으로 local stroage, session storage보다 저장 용량이 크고 이 둘과는 달리 비동기로 동작이 가능한 Cache Storage 혹은 indexedDB 사용이 권장된다.

image

위 그림은 브라우저에서 리소스 요청 시 캐싱 순서를 보여주는데, 클라이언트 사이드 캐싱이 필요한 이유가 잘 드러난다. 클라이언트에서의 요청이 적절한 캐싱 전략으로 클라이언트 내부에서 처리된다면 유저는 좀 더 빠른 결과를 받아볼 수 있고 서버는 부하가 덜해질 것이다.

2. CORS를 Proxy로 우회하기

브라우저가 origin 서버로 direct하게 API를 요청하는 게 아닌, Proxy 서버가 중간에서 이 요청을 받아서 처리해주는 방식. 그렇다면 Proxy 서버는 어떻게 CORS 문제 없이 origin 서버와 통신이 가능한 것인가?

CORS는 브라우저의 검열

What are CORS proxies, and when are they safe?

교차 출처 리소스 공유 (CORS)

CORS 체제는 브라우저와 서버 간의 안전한 교차 출처 요청 및 데이터 전송을 지원합니다.

CORS는 브라우저가 요청을 검열하는 것. 따라서 일단 Proxy 서버로 CORS 문제 없이 요청을 보내고나면, Proxy - Origin 서버간 통신은 CORS로부터 자유롭다. 애당초 origin서버는 CORS 허용 설정 (response에 Access-Control-Allow-Origin 지정)이 안 되어있는 것뿐이지 Public access까지 제한된 것이 아니기 때문에 Proxy 서버가 Origin 서버와 통신하는데는 아무런 문제가 없다.

image

Proxy 사용에는 대가가 따른다

Proxy 서버는 HTTP 요청/응답을 읽고 무엇이든 할 수 있다. 따라서 내 요청이 탈취/조작되어도 아무런 문제가 없는 경우에 한하여 Proxy를 이용하고 가능한 직접 만든 Proxy 또는 인증된 단체의 Proxy 서버를 사용할 것.

3. 관심사의 분리 (SoC, Seperation of Concerns)

  • SoC는 클린코드의 근본이다. 클린 코드를 위한 여러가지 방법론들이 결국에는 관심사의 분리를 통한 유지보수의 편의를 높이는 방향이다. 여기서 관심사란, 일반적으로 하나의 모듈이 수행하고자 하는 목적을 뜻한다.

  • 하나의 함수에 한 가지 목적만이 존재한다면, 나중에 이 코드가 수정되어야 할 이유도 단 하나뿐이게 된다. 따라서 SoC를 준수하면 리팩토링 시 발생하는 사이드 이펙트의 우려를 줄일 수 있어 유지보수가 편해진다.

  • 개인적으로 리팩토링 시 발생하는 사이드 이펙트에 고통받고 테스트 코드를 공부해야겠다고 다짐한적이 있다. 테스트 커버리지 100%를 달성하면 아무런 거리낌 없이 리팩토링을 할 수 있다는 얘기를 들었던 까닭이다. 그런데 알고보니 내게 필요한 건 테스트 코드 작성이 아닌 SoC 준수였다.

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

  • KISS (Keep It Simple, Stupid)

리액트의 관심사

  1. UI
  2. UI를 바꾸는 로직

a) Presentational - Container 패턴으로 관심사 분리하기

  • Presentational 컴포넌트 → (자식, UI)
  • Container 컴포넌트 → (부모, 로직)
  • 이후 Container 컴포넌트에서 상태를 Props로 Presentational 컴포넌트에 전달

UI가 조금 다른 Signup/Signin 컴포넌트가 있다. Custom Hook으로 UI와 로직을 분리했을 땐 공통적인 UI 부분의 재사용 처리가 조금 애매해서 그냥 사용했는데, Presentational - Container 패턴을 적용하면 깔끔하게 처리할 수 있었다.

  • Custom Hook
import useFormValidation from '../hooks/useFormValidation'
 
function Signin() {
  const {
    formData,
    isDisabled,
    handleEmailChange,
    handlePasswordChange,
    handleSubmitSignin
  } = useFormValidation()
 
  return (
    <form onSubmit={handleSubmitSignin}>
      <label>
        이메일
        <input
          data-testid="email-input"
          value={formData.email}
          onChange={handleEmailChange}
        />
      </label>
 
      <label>
        비밀번호
        <input
          data-testid="password-input"
          value={formData.password}
          onChange={handlePasswordChange}
        />
      </label>
 
      <button data-testid="signin-button" disabled={isDisabled} type="submit">
        로그인
      </button>
    </form>
  )
}
 
export default Signin
 
import useFormValidation from '../hooks/useFormValidation'
 
function Signup() {
  const {
    formData,
    isDisabled,
    handleEmailChange,
    handlePasswordChange,
    handleSubmitSignup
  } = useFormValidation()
 
  return (
    <form onSubmit={handleSubmitSignup}>
      <label>
        이메일
        <input
          data-testid="email-input"
          value={formData.email}
          onChange={handleEmailChange}
        />
      </label>
 
      <label>
        비밀번호
        <input
          data-testid="password-input"
          value={formData.password}
          onChange={handlePasswordChange}
        />
      </label>
 
      <button data-testid="signup-button" disabled={isDisabled} type="submit">
        회원가입
      </button>
    </form>
  )
}
 
export default Signup
  • Presentational - Container 패턴
import { useState } from 'react'
import Presentational from './Presentational'
import { signupAPI, signinAPI } from '../apis/auth'
import { FormData } from '../types'
 
interface Props {
  type: 'signIn' | 'signUp'
}
 
function Container({ type }: Props) {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: ''
  })
 
  const isDisabled = !!(
    formData.password.length < 8 || formData.email.indexOf('@') === -1
  )
 
  const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    const dataset = event.target.dataset['testid']
    const target = dataset?.split('-')[0]
    if (target === 'email' || target === 'password') {
      setFormData({ ...formData, [target]: event.target.value })
    }
  }
  const handleOnSubmit = async (
    event: React.FormEvent<HTMLFormElement>,
    type: 'signIn' | 'signUp'
  ) => {
    event.preventDefault()
    type === 'signIn'
      ? await signinAPI({ email: formData.email, password: formData.password })
      : await signupAPI({ email: formData.email, password: formData.password })
  }
 
  return (
    <Presentational
      type={type}
      formData={formData}
      isDisabled={isDisabled}
      handleOnChange={handleOnChange}
      handleOnSubmit={handleOnSubmit}
    />
  )
}
 
export default Container
function Presentational({
  type,
  handleOnSubmit,
  formData,
  handleOnChange,
  isDisabled
}: any) {
  return (
    <form onSubmit={e => handleOnSubmit(e, type)}>
      <label>
        이메일
        <input
          data-testid="email-input"
          value={formData.email}
          onChange={handleOnChange}
        />
      </label>
 
      <label>
        비밀번호
        <input
          data-testid="password-input"
          value={formData.password}
          onChange={handleOnChange}
        />
      </label>
 
      {type === 'signIn' ? (
        <button data-testid="signin-button" disabled={isDisabled} type="submit">
          로그인
        </button>
      ) : (
        <button data-testid="signup-button" disabled={isDisabled} type="submit">
          회원가입
        </button>
      )}
    </form>
  )
}
 
export default Presentational

b) Custom hook으로 관심사 분리하기

토스ㅣSLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트

image

  • 부끄럽게도 지금껏 내가 짠 Custom Hook에는 재사용성이 고려되지 않았다. UI와 로직을 분리하는 측면으로 동작하여 복잡도만 아주 조금 낮춰줄 뿐이었다.
  • 무언가 잘못되었음은 인지했으나 개선책이 떠오르질 않아 답답하던 차에 팀원들의 재사용 가능한 Custom hook 디자인을 보고 많은 도움을 받았다.
  • 변경 전
const Search = () => {
  const {
    onSubmitHandler,
    onChangeHanlder,
    onKeyDownHandler,
    searchState,
    focusedItem,
    searchItemCnt,
    onMouseDownHandler
  } = useSearch()
	// 이런 식으로 하나의 UI 컴포넌트에 그에 상응하는 Custom Hook을 두어 로직을 숨기기에 급급했다.
 
  return (
		UI...
  )
}
  • 변경 후
const Search = () => {
  // 여러개의 재사용 가능한 Custom Hook으로 쪼갰다.
  // 후술할 Context API + useReducer의 구조가 더해지면 더 깔끔하고 분리가 잘 된 코드를 작성할 수 있다.
  const [searchInput, setSearchInput] = useState('')
  const { ref, isFocus, onFocusHandler } = useClickOutside()
  const { cachedData, fetchCached, fetchDebounced } = useCache(SEARCH_STORAGE)
  const { focusIndex, searchItemCnt, onKeyDownHandler } = useKeyboard({
    onEnter() {
      let autoSearch
      if (searchInput.length === 0) {
        if (focusIndex === -1) {
          alert('값을 입력해주세요')
          return
        }
        autoSearch = getRecentKeywords()[focusIndex]
      } else {
        autoSearch = cachedData[focusIndex - 1]?.name
      }
      setSearchInput(autoSearch === undefined ? '' : autoSearch)
      fetchCached(autoSearch, true)
    }
  })
    
    return (
    	UI...
    )
  }
 

4. React S.O.L.I.D

[번역] React에 SOLID 원칙 적용하기

프론트엔드와 SOLID 원칙

React 컴포넌트와 추상화

This is the Only Right Way to Write React clean-code - SOLID

The S.O.L.I.D Principles in Pictures

OOP는 아직 개념적으로 부족한게 많다. 조영호님의 오브젝트와 같은 책으로 더 공부해야 할 부분이다. 그래도 지금 당장 React에 간단히 적용해볼 정도로 공부하는 건 시야확장 측면에서 도움이 될 것이라고 믿었다.

리액트 프로젝트의 결합도를 관리하는 방법

아래에서 클래스라는 용어는 함수, 메소드, 모듈로 통용될 수 있다.

1) 단일 책임 원칙 (SRP, Single Responsibility Principle)

  • 모든 클래스는 정확히 한 가지 작업을 수행해야 한다.

  • 만약 클래스가 여러개의 책임을 갖고 있다면 이 중 한 가지 책임을 수정할 때 다른 책임에 영향을 끼치고 버그가 발생할 수 있다.

// 비즈니스 로직과 UI가 혼재되어 있는 컴포넌트
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>
  );
};
// useUsers hook으로 state, effect 관련 로직을 처리
// 그럼에도 짧고 간단한 비즈니스 로직이 컴포넌트에 잔존한다.
// 이를 useActiveUsers hook으로 한 번 더 감싸서
// 최종적으로 렌더링에 필요한 상태만 주입받아 사용하라
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>
  );
};

2) 개방-폐쇄 원칙 (OCP, Open-closed Principle)

  • 클래스는 확장에는 개방되어 있고, 수정에는 닫혀 있어야 한다.

  • 클래스의 현재 동작을 수정하는 행위는 이 클래스를 사용하고 있는 다른 모든 코드에 영향을 줄 수 있다.

  • 따라서 새로운 동작의 클래스가 필요할 땐 클래스A의 동작을 수정(추가)할 게 아니라 클래스 A를 확장하여 클래스 B를 만들어라.

// <Header> 컴포넌트를 다른 곳에서 조금씩 변형하여 재사용해야 하는 상황.
// 만약 <Header> 컴포넌트를 직접적으로 수정하면,
// <Header>를 재사용하는 상황이 많아질수록 <Header>는 취약해지고 결합도가 높아진다.
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 />
  </>
);
// 컴포넌트 함성을 이용하여 <Header>를 재사용해야하는 컴포넌트에게 책임을 전가하라.
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 />
  </>
);

3) 리스코프 치환 원칙 (LSP, Liskov Substitution Principle)

image

  • 만약 자식 클래스가 부모 클래스의 똑같은 동작을 수행하지 못한다면 이는 버그로 이어질 수 있다.

  • 클래스A로부터 클래스B를 생성하면 클래스A는 부모 클래스가 되고, 클래스B는 자식 클래스가 된다. 자식 클래스는 부모 클래스가 할 수 있는 모든 행동을 마찬가지로 할 수 있어야 한다. 이것이 상속이다.

  • 이 원칙은 일관성을 강요해서 부모 클래스와 자식 클래스가 에러 없이 동일한 방식으로 사용될 수 있기를 목표로한다.

  • 여기까지는 이론적인 설명이고, 아키텍처 혹은 React 관점에서는 좀 더 넓은 범주에서 다양하게 설명된다.

아래는 React에서 LSP란 무엇인지를 설명하는 내용들이다. 관점이 다들 다르다.

  1. LSP는 클래스간 상속에 관한 원칙이고, React는 상속보단 합성을 권장하니 React에서는 LSP를 적용하기 어렵다.

  2. is-a 만족 여부로 상속 관계인지를 판변할 수 있다. 사과는 과일이다 처럼 명확한 관계를 갖는 것이 상속이다. 상속으로 이어진 관계에서 예상 못할 행동을 하면 안된다. 이를테면 다음과 같다. GET method 의 REST API로 정의했는데 실제 동작에선 DB 상태를 변경하면 안된다. ApiErrorBoundary라는 컴포넌트에서 Api와 관계없는 동작을 수행하고 있으면 추후 관리에 어려움이 발생할 수 있다.

  3. <input> 태그를 사용중인 <SearchInput> 컴포넌트가 있다면 여기서 SearchInput 컴포넌트가 자식 클래스고, input 태그가 부모 클래스에 해당한다고 볼 수 있다. 이럴땐 props가 잘 전달되도록 유의하여 두 클래스간 동작?에 차이가 없도록 하라.

function SearchInput({prop1, prop2, ...rest}) {
 
  return (
   <input /> 
    {...rest}
   )
}

4) 인터페이스 분리 원칙 (ISP, Interface Segreagtion Principle)

  • 클라이언트는 사용하지 않는 인터페이스에 의존해서는 안 된다. React에서 이는 곧 컴포넌트에서 사용하지 않는 Props에 의존해서는 안 됨을 의미.

  • 컴포넌트에 딱 필요한 만큼의 타입스크립트 Interface 지정하기. 불필요한 props 및 타입 제거.

5) 의존성 역전 원칙 (DIP, Dependency Inversion Principle)

리액트에서 의존성 역전 원칙 적용하기(feat. 좋은설계란무엇일까?)

image

  • High-level Module(or Class): Class that executes an action with a tool.

  • Low-level Module (or Class): The tool that is needed to execute the action

  • Abstraction: Represents an interface that connects the two Classes.

  • Details: How the tool works

  • 클래스(high-level module)는 도구(low-level module)와 융합되어서는 안 된다. 그보다는 도구가 클래스를 가리키는 인터페이스와 융합되어야 한다.

  • 도구 -> 클래스 (X), 도구 -> 인터페이스 -> 클래스 (O)

  • 클래스와 인터페이스는 도구가 어떻게 동작하는지(Details)를 알아서는 안 된다.

  • DIP는 인터페이스를 도입하여 상위 클래스가 하위 클래스에 대한 의존성을 줄이는 것을 목적으로 한다.

그렇다면, 이 원칙이 의존성의 방향을 관리하고자 하는 원칙이라는 것은 알겠는데, 왜 '역전'이라는 이름을 사용한 것일까요? 이는 객체지향이 아닌, 전통적인 방식의 개발에서는 상위정책에 해당하는 모듈이 하위정책에 해당하는 모듈에 의존하는 경향이 있었기 때문에 그렇습니다. 의존성의 방향을 상위 -> 하위에서 하위 -> 상위로 바꾸었기 때문에 이것을 '역전'했다고 볼 수 있는 것입니다.

  • DIP는 IoC의 실현 방법 중 하나로 소스 코드의 구성 요소들 간 의존성을 뒤집는 것을 뜻한다. 구체(concretion)에 의존하지 않고, 추상(abstractions)에 의존하는 것을 지향한다.

  • How, 과정에 집중하는 구현은 추후 변경될 여지가 많다. 그에비해 What, 결과에 집중하는 추상(Interface)은 변경될 여지가 적다. 최소한의 Interface만 준수하고 내부 구현사항을 커스텀하는 식으로 코딩하면 유연한 대응이 가능해진다.

  • 의존성 역전 원칙을 적용한 이후에 변경의 전파가 얼마나 효율적으로 차단되었는가? 코드의 수정이 생길 때, 변경사항이 얼마나 많이 줄어들었는가?

  • 리액트에서는 children을 이용하면 손쉽게 의존성을 역전시킬 수 있다.

// 이 코드는 <LoginForm> 컴포넌트가 API 모듈(구체)에 의존하고 있다.
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>
  );
};
-------------
// API 모듈과의 직접적인 의존을 끊고 onSubmit props로 필요한 기능을 주입 및 추상화.
// 이제 LoginForm 컴포넌트는 api 모듈에 직접적으로 의존하지 않는다.
// LoginForm 컴포넌트는 추상에 의존하고 있다. 이 컴포넌트의 구체에 대한 책임은 상위 컴포넌트로 넘어간다.
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 UI
}
// <ConnectedLoginForm> 컴포넌트는 api와 LoginForm 사이의 접착제 역할.
// 복잡한 의존도 없고 서로 완전히 독립적이어서 반복적인 테스트 가능.
// LoginForm과 api가 모두 합의된 공통 추상(type Props)을 준수하는 한 안전하게 동작함
// HOC(고차 컴포넌트), Presentational - Container 패턴과 유사
import api from "~/common/api";
 
const ConnectedLoginForm = () => {
  const handleSubmit = async (email, password) => {
    await api.login(email, password);
  };
 
  return <LoginForm onSubmit={handleSubmit} />;
};

제어의 역전 (IoC, Inversion of Control)

이제부터 이 컴포넌트는 제 겁니다

  • 코드의 흐름을 제어하는 주체가 바뀌는 것을 뜻한다. don’t call me, I’ll call you. 개발자가 원하는 대로 컴포넌트를 제어할 수 있도록 코드를 짜는 것.
  • 컴포넌트 내부에 의해서만 렌더링 결과물을 만들어내지 않고, 컴포넌트를 사용하는 입장에서 렌더링 방식을 컨트롤하는 것. 유연하고 재사용가능한 컴포넌트 작성을 꾀한다.
  • 렌더링 IoC → Render Props, Compound Components 패턴
  • 상태관리 IoC → Controlled Props, Props Getter, state Reducer

제어의 역전(Inversion of Control, IoC)은 소프트웨어 디자인 패턴 중 하나로, 어떤 프로세스나 객체가 실행 흐름의 제어 권한을 갖지 않고, 대신에 외부에서 해당 프로세스나 객체에게 제어를 위임하는 것을 의미합니다. 이를 통해 코드의 유연성과 확장성이 향상되고, 코드의 결합도를 낮출 수 있습니다. 의존성 역전(Dependency Inversion)은 IoC의 구현 방법 중 하나로, 소스 코드의 구성 요소들 간의 의존성을 뒤집는 것을 의미합니다. 즉, 고수준 모듈은 저수준 모듈에 의존하지 않고, 저수준 모듈은 고수준 모듈에 의존하도록 구성하는 것입니다. 제어의 역전과 의존성 역전은 서로 관련이 있지만, 다른 개념입니다. 제어의 역전은 실행 흐름의 제어를 바깥에서 내부로 넘기는 것이며, 의존성 역전은 소스 코드의 의존성을 바깥에서 내부로 뒤집는 것입니다. 따라서, 제어의 역전과 의존성 역전은 모두 소프트웨어 디자인에서 중요한 개념이며, 각각의 장점을 살려서 적절하게 적용할 수 있어야 합니다. by ChatGPT

의존성 주입 (DI, Dependency Injection)

  • DI is about how one object acquires a dependency

  • 필요로하는 오브젝트를 스스로 생성하는 것이 아닌 외부로부터 주입받는 기법

리액트의 의존성 주입 with Context API (dependency injection)