728x90
반응형

이벤트 루프를 그려본다면?

 

이벤트 루프 => 싱글 스레드로 동작하는 js에서 비동기 동작의  처리를 가능하게 해주는 교통 정리 시스템


이벤트 루프의 실행순서

1.  callstack에 쌓인 실행컨텍스트를 실행 

2. callstack이 비워짐

3. microtaskQueue를 확인하고 비워질때까지 실행

4. macroTaskQueue에 있다면 하나만 실행

5. 다시 callstack 확인 후 비우기 시작

 

macroTaskQueue에 쌓이는 친구들은 대표적으로 setTimeout
micrtoTaskQueue에 쌓이는 친구들은 대표적으로 promise, queueMicroTaskQueue

 


js로 이 함수가 실행이되면 어떻게 될까?

그러기 위해서 잠깐 html 렌더링 과정을 살펴보고 와야 한다

 

 

html 문서는 위의 그림처럼 파싱이 되고 화면에 그려지게 된다

js로 위의 함수가 실행이 되면 함수의 실행에 따라 microTask 혹은 macroTask에 쌓이게 된다

이때 중요한 것은 MacroTaskQueue는 하나의 큐만 비우고 이후 과정을 실행한다

그리고 콜스택과 마이크로 태스크큐를 싹 비운다 

그 다음에

브라우저가 렌더링을 할지 말지를 결정한다

이때 중요한 것이 시간이다

화면은 ux를 위해 60frame을 준수해야 하고 16ms마다 렌더링이 된다(필요하다면)

이래서 앞의 과정 microTaskQueue가 오래점유하고 있거나 콜스택이 오래 실행이 된다면 렌더링은 제때 일어나지 못하고

사용자에게는 버벅이는 과정이 일어난다

 

여기서 렌더링 직전에 실행되는 함수가 RAF(RequestAnimationFrame)이다

raf를 사용하면 렌더링 직전에 콜백이 실행되므로 

사용자에게 좀 더 부드러운 경험을 제공하려면 raf 함수를 사용하는 것이 좋다

728x90
반응형
728x90
반응형

프로젝트들에 모노레포를 적용하면서 했던 의사결정과 pnpm에 대한 이모저모

 

모노레포 적용이 필요했던 배경

  • 멀티 레포로 관리되던 상황이었어서 공통의 util / 디자인 컴포넌트를 사용하기에 생각보다 큰 리소스가 예상되었다
  • 비즈니스의 흐름을 이해하기 어렵다
    • 피쳐 성격상 a웹과 b웹에서 같이 작업이 이루어지는 경우가 있을 수 있다
    • 이때 pr등에서 확인하기가 어렵고 여러 ide 창을 켜놔야 하는 경우도 있었다

=> 모노레포를 적용하고 이를 통해 위의 상황을 개선해보자!!


그래서 선택지는 총 3개

  • nx
  • pnpm
  • yarn berry

기존의 프로젝트 들은 yarn classic을 쓰고 있었다


처음 시도했던 것은 nx, 그러나

  • nx는 꽤나 어려웠다
  • 고점은 높아보였지만 러닝커브가 쉽지 않아 보였다
  • 회사 스테이지상 학습을 위한 시간을 확보하기는 매우 어렵다 <- 개인의 희생을 강요할 수 없다

이제 선택지는 yarn berry와 pnpm만 남았다

어찌할 것인가

 

pnpm을 선택하기로 했다

  • pnpm을 다뤄봤던 팀원이 있었고
  • npm trends를 믿어보기로 했다

 

(이 당시 잘못 알고 있었던 것은 yarn berry는 node_modules가 존재하지 않아 rn에서 쓸 수 없을까 였었다. 

yarn berry에서도 hoisted를 통해 node_modules를 생성할 수 있다)

 

pnpm은 빠르다

왜 빠른가

  • 설치시 pnpm store에 해당 라이브러리 들을 설치한다
  • a 프로젝트에서 라이브러리를 설치시 store로 부터 하드링크를 연결한다 <- 이미 설치되어 있다면 재설치를 하지 않는다 
  • node_modules/.pnpm에 store에서 가져온 라이브러리들이 존재한다
  • 비슷한 개념이 심링크 이다
    • 심링크는 바로가기로 연결만 한다
    • 이 심링크를 사용하는 것은 아래에서 확인하자
node_modules
├── foo -> ./.pnpm/foo@1.0.0/node_modules/foo
└── .pnpm
    ├── bar@1.0.0
    │   └── node_modules
    │       ├── bar -> <store>
    │       └── qar -> ../../qar@2.0.0/node_modules/qar <- /node_modules/.pnpm/qar@2.0.0/node_modules를 심링크
    ├── foo@1.0.0
    │   └── node_modules
    │       ├── foo -> <store>
    │       ├── bar -> ../../bar@1.0.0/node_modules/bar  <- /node_modules/.pnpm/bar@1.0.0/node_modules를 심링크
    │       └── qar -> ../../qar@2.0.0/node_modules/qar  <- /node_modules/.pnpm/qar@2.0.0/node_modules를 심링크
    └── qar@2.0.0
        └── node_modules
            └── qar -> <store>

 

 

1. 프로젝트에서 foo@1.0.0 라이브러리를 추가한다

2. node_modules/.pnpm에 foo@1.0.0이 하드링크로 추가된다 (이때 원본은 .pnpm의 store에 위치한다)

    * package를 import하는 방법을 정할 수 있는데 가장 권장되는 수단은 COW(CopyOnWrite) file system 이다

    * COW는 기본적으로 주소값 복사를 하고 변경이 필요한 경우 변경점만 별도로 디스크를 할당한다

3. foo@1.0.0의 내부서 의존하는 bar@1.0.0 / qar@2.0.0이 .pnpm에 하드링크로 추가된다 

4. foo@1.0.0의 내부 라이브러리들은 각 .pnpm에 설치된 라이브러리들에 심링크로 연결되어 있다 

5. foo@1.0.0은 프로젝트에서 직접 참조되므로 .pnpm의 외부로 이동된다. 이때 .pnpm을 심링크로 연결한다

 

COW(CopyOnWrite)

 

(npm은 캐싱 없이 매번 설치마다 node_modules에서 디스크 공간을 차지해야 한다)

 

 

그 외에 장점은

  • 유령 의존성을 없앤다
    • 패키지 별로 node_modules를 가지고 버전 명세가 명확하다
    • hoisting이 되지 않는 구조에서 package.json에 명세 없이 코드상에서 import를 하는 것은 실행이 되지 않는다
유령 의존성이란?

내 프로젝트에서 쓰고 있지만 package.json에는 없는 애


node_modules/
	lodash/
    a/package.json
    b/package.json/lodash



패키지 a의 코드에서
import lodash from 'lodash' <- 요 코드를 실행할 수 있다 (npm과 yarn classic에서는 실행가능)

=> 런타임에서 에러발생이 가능한 환경이 된다

 


rn에서 pnpm을 사용할 때 hoisting을 사용해야 한다

왜냐하면 rn의 하위 버전에서는 symlink가 동작하지 않고 메트로 + 기존 라이브러리에서 node_modules의 계층형 구조를 인식하지 못한다

빌드시 매번 node_modules에 라이브러리가 존재하지 않는다는 에러를 마주할 수 있다

아래와 같이 rn 하위버전 metro 설정에서 override를 해줘야 할 수 도 있다

metro.config.js

const defaultConfig = getDefaultConfig(projectRoot);
const {
  resolver: { sourceExts, assetExts },
} = defaultConfig;

function resolveFromProject(modulePath) {
  return path.dirname(require.resolve(`${modulePath}/package.json`, { paths: [projectRoot] }));
}

const reactDir = resolveFromProject('react');

const config = {
  resolver: {
    extraNodeModules: {
      react: reactDir,
    },
  },
};

module.exports = mergeConfig(defaultConfig, config);

 

but

 

store를 통한 의존성 설치시의 캐싱활용은 장점이 될 수 있다

유령의존성 제거 장점을 취하지 못한 것은 아쉬울 수 있다

ci/cd에서의 장점은 .pnpm store를 github에서 캐싱해둠으로써 매번 새로 설치하지 않아도 되는 장점을 이룰 수 있다

(아래는 캐싱 예시)

- name: Cache pnpm store
  uses: actions/cache@v3
  with:
    path: ~/.pnpm-store
    key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}

 


 

.npmrc에서

node-linker를 hoisted로 설정할지 옵션을 설정할 수 있다

 

모노레포 프로젝트들의 버전을 override해줄 수 있다

이때 각 프로젝트의 node_modules에 라이브러리들이 설치가 된다

patch는 npm 환경에서 node_modules 내부 라이브러리를 직접 바꾸고 싶은경우 사용되는 방식이다

pnpm에서는 아래와 같이 설정할 수 있다

package.json

"pnpm": {
    "overrides": {
      "a>react": "19.0.0",
      "react": "18.2.0",
    },
    "patchedDependencies": {
      "c": "patches/c.patch",
      "d": "patches/d.patch",
    }
  },

 

root/
 ├─ apps/
 │   ├─ web/
 │   └─ admin/
 ├─ packages/
 │   ├─ ui/      (Shared Component Library)
 │   ├─ utils/   (Shared Business Logic)
 │   └─ config/  (ESLint, TSconfig shared)
 └─ pnpm-workspace.yaml

 

* apps는 실제 서비스이다

* packages는 공유 라이브러리 또는 컴포넌트 들이다

* pnpm은 내부 패키지에 대해서는 store를 거이지 않는다.

* pnpm은 내부 패키지들을 softlink 처리한다

* packages를 수정하면 바로 반영된다

 


 

pnpm의 선택은 단순 속도 문제는 아니었고

프로젝트 구조, 개발 경험(DX), 라이브러리의 선택 기준등 

생각보다 많은 것이 엮인 결정이었다.

728x90
반응형

'Frontend > React' 카테고리의 다른 글

Redux persist와 migration  (0) 2025.11.27
[번역] React Atomic하게 바라보기  (0) 2024.05.19
React patterns 🤔  (0) 2023.06.06
React 공식문서 주요개념 살펴보기  (1) 2022.05.26
React what is JSX? (번역글) 🤔  (2) 2022.04.27
728x90
반응형

개념

react 어플리케이션을 만들때면 전역 상태 관리를 위해 redux를 사용해야 할 때가 있습니다

 

이 전역상태 역시 메모리 상의 상태이기 때문에, 페이지를 새로고침 하거나 앱 프로세스가 초기화되면 전부 초기화 됩니다.

이렇게 되면 로그인이 풀려버리는 등의 상황을 마주할 수 있어요

그렇지 않기 위해서

웹에서는 localStorage, sessionStorage, cookie등 브라우저 스토리지에 이를 저장해두고 동기화를 하여 사용하거나

react-native에서는 fileSystem 혹은 asyncStorage에 저장을 해두어 새로고침이나 앱 종료 후에도 상태를 유지할 수 있습니다.

 

이 과정을 도와주는 라이브러리가 redux-persist 입니다

redux-persist에서는  stoarge engines을 변경함으로써 다양한 환경에서 redux의 값을 저장해줄 수 있습니다

가령 아래와 같은 방식입니다.

// 예시를 위해 몸을 심하게 비튼 코드

const getStorage = () => {
  switch(환경) {
  	case 일렉트론
    	return createElectronStorage()
    case 웹 로컬 스토리지
        return localStorage // import storage from 'redux-persist/lib/storage'
    case 웹 세션 스토리지 
    	return sessionStorage // import storage from 'redux-persist/lib/storage/session'
    case 리액트 네이티브
    	return asyncStorage // import storage from '@react-native-community/async-storage';
    등등
  }
}


const persistConfig = {
	key: 'root',
    storage: getStorage() // 이곳의 storage만 바꿔준다면 다양한 환경에서 사용가능
};

const persistedReducer = persistReducer(persistConfig, rootReducer)

 

이렇게 사용을 할 때 주의해야 하는 점이 있습니다

스토리지에 저장된 값의 구조(타입)과 현재 코드에서 사용되는 형태가 달라지게 된다면 어떻게 될까요?

상황에 따라 브라우저에서는 런타임에러가 발생할 수 있고 

앱에서는 앱의 크래시가 발생할 수 있습니다

 

이러한 상황을 막기 위해 migration을 적절히 해주어야 합니다

이 글에서는 실무에서 migration을 적용하여 기존의 버그들을 피한 상황을 서술하려 합니다

 

실무에서 발생했던 상황은 아래와 같습니다.

redux의 값을 수정했고 코드푸시를 나간이후에 기존 유저의 앱에서 crash가 나는 현상이 발생되었다

 


코드 예시

에러가 나는 상황

기존에 redux-persist를 통해 값이 저장되어 있던 형태

type TodoState = string[] // ['밥먹기', '출근하기']


const todoPersistConfig = {
  key: 'todo',
  storage,
};


const rootReducer = combineReducers({
  todos: persistReducer(todoPersisConfig, todo),
});

 

값을 가져와서 쓰던 형태

const todos = useSelector((state) => state.todos);

return (
  <View>
  	{todos.map((todo) => (
      <Text>{todo}</Text>
    )}
  </View>
)

 

값이 저장되어 있던 형태가 변경되었고 이것이 코드푸시를 통해 배포가 나감

type Todo = { id: number, title: string };

type TodoState = {
  list: Todo[];
}

const todoPersistConfig = {
  key: 'todos',
  storage,
};


const rootReducer = combineReducers({
  todos: persistReducer(todoPersisConfig, todo),
});

 

이때 마이그레이션을 하지 않으면 런타임에서 에러가 발생 

const { list: todos } = useSelector((state) => state.todos);  // <- redux-persist를 통해 storage에
// 저장되어 있던 값은 ['밥먹기', '출근하기']
// list가 존재하지 않음
// 아래 런타임에서 에러 발생

return (
  <View>
  	{todos.map((todo) => (
      <Text>{todo.title}</Text>
    )}
  </View>
)

 

새로운 유저는 영향 x, 기존에 redux에 값이 저장되어 있던 유저만 영향을 받는 상황

=> qa를 어렵게 하고 예측 불가능한 상황이 되어 버림

 위와 같은 이유로 안쓰는 키값을 제거하거나 타입의 변경시 에러 방지를 위해 마이그레이션이 필요합니다

 


마이그레이션 방법

persistConfig를 정의할때 migration이 필요할 경우 migration 코드와 version을 올려주면 됩니다

import { createMigrate } from 'redux-persist'

const migrations = {
  1: (state) => {
  	if (state && typeof state === 'object') {
      return {
        ...state,
        todo: { 
        	list: []
        }
      };
    }
  } 
}


const persistConfig = {
  key: 'root',
  storage,
  version: 1, // 버전을 1로 증가하여 새로운 마이그레이션 트리거
  migrate: createMigrate(migrations, { debug: __DEV__ }),
};

 

 

728x90
반응형

'Frontend > React' 카테고리의 다른 글

pnpm과 이모저모  (0) 2025.11.27
[번역] React Atomic하게 바라보기  (0) 2024.05.19
React patterns 🤔  (0) 2023.06.06
React 공식문서 주요개념 살펴보기  (1) 2022.05.26
React what is JSX? (번역글) 🤔  (2) 2022.04.27
728x90
반응형

Atomic Design, by Brad Frost

 

Brad Frost의 아토믹 디자인 원칙에 익숙하지 않다면 잠시 멈추고 그의 블로그 혹은 그의 책을 읽어보세요. 근사한 디자인 시스템이고 적절히 수행되었을때 여러분 어플리케이션의 화면 구성요소들이 아름답고 간편하게 구성될 수 있습니다.


 

아토믹 디자인이 간단하게 말해서 무엇인가요?

아토믹 디자인은 사용자 인터페이스를 더 작고 단순한 요소들로 분리하는 개념입니다. 아토믹 디자인에는 다섯개의 각기 다른 레벨이 존재합니다. atoms, molecules, organism, templates, pages.

하나의 atom은 input 필드 혹은 버튼이 될 수 있습니다. 이러한 atom들은 검색 molecule을 만들기 위해 로고 atom들과 결합되어 질 수 있고 navbar organism을 만들기 위한 네비게이션 molecule을 구성할 수 있습니다.

 

atom, molecule, organism들을 사용하여 page를 위한 template을 구성할 수 있고 디자인 process를 더 빠르게 만들 수 있습니다


 

이게 어떻게 React와 잘 맞을 수 있을까?

1년 반정도 React를 빡세게 다뤄왔습니다. 그 당시 나는 아키텍쳐 실수들을 공유해왔고 다른 사람들의 실수를 보면서 배워왔습니다.

React는 KISS(Keep it simple, stupid)를 정말 추구하고 '한 가지 일만 잘하게 해라'와 같은 것들을 배웠습니다.

React 어플리케이션을 architecting할때 아토믹 디자인 패턴을 따르는 이러한 방법론을 따르는 것을 보장합니다.

 

Atom으로서의 단순한 컴포넌트들

컴포넌트들은 가능하면 바보여야 합니다. 어플리케이션의 비즈니스 로직에 대해서는 알지 못해야 하고 그들이 의미하는 것은 단순히 보여주는 것이어야 합니다.

Three distinct atoms, that when combined, can make a search molecule

 

우리가 Button 컴포넌트를 만들고 있다고 해봅시다. Button은 label을 가져야 하고 type, onClick 핸들러도 가져야 할 겁니다.

버튼은 디자인 된대로 보여지게 지게 되고 onClick을 다루게 됩니다.

onClick이 발생했을 때 API 호출을 다루지 않아야 합니다. 그게 이 container의 역할 이빈다.

 

This is what makes it an atom. It’s a small, contained, and simple component of our application.

 

React에서의 Molecules 

TextInput 컴포넌트와 Button 컴포넌트로 검색 모듈을 만들어야 한다고 해보겠습니다. 두개의 컴포넌트들을 결합할때 molecule로서 동작하는 검색 컴포넌트를 만들 수 있습니다. 만약 가능하다면 atom과 같이 molecule에서도 비즈니스 로직에 대해 몰라야 합니다. 버튼 컴포넌트가 눌러졌을때 onSearchClick 핸들러를 가질 수 있지만 TextInput 컴포넌트에 어떤게 입력되었는지에 따라 어떠한 결과라도 fetch하지 않아야 합니다. 이것이 컨테이너의 역할 입니다.

 

Search Molecule

 

Organimsms, Everywhere

Organism은 page들을 그룹핑하기 위해 사용되는 복잡한 Ui 구성요소들과 더 유사합니다. 그들이 molecule과 atom보다 더 복잡하기 때문에 organism들을 비즈니스 로직들을 처리해야 할 수 있습니다.

 

이커머스 웹사이트와 상품 페이지가 있다고 가정해볼게요. Navbar organism과 상품 molecule를 가지고 있는 ProductGrid organism으로 만들어졌습니다. 아래의 Navbar organism을 보시죠

Navbar Organism

 

 

Navbar Organism은 이전에 만든 Search molecule, Navigation molecule, Logo atom으로 만들어졌습니다. 이러한 경우 organism은 비즈니스 로직을 처리할 필요가 없습니다. Organism이 구성될 때 template과 page들에서 비즈니스 로직들을 다룰 수 있도록 만들 수 있습니다

 

컨테이너들은 Template/Page들과 같아야 하나요?

맞습니다. Container는 organism, molecule, atom들을 가지고 있고 복잡한 로직들을 다룹니다. Template과 Page는 또한 organism, molecule, atom들을 가집니다. 작은 컴포넌트 요소들을 결합하면 어플리케이션의 interface를 만들수 있습니다.

 

A template, which can be turned into a page

 

그래서 어느 Atomic 단계에서 비즈니스 로직을 다뤄야 하는데?

organism을 가지게 되면 상황은 조금 더 복잡해 집니다. 위에서 언급한 이커머스 상품 페이지에서 Navbar organism은 비즈니스 로직을 수행할 필요가 없지만 ProductGrid organism은 만약 우리가 paginate를 해야한다면 상품 fetch역할을 요구받게 됩니다. 상품에 대해 구체적인 파라미터들을 추가할 수있고 추가한 파라미터들을 가지고 API호출을 하는 Filter organism도 있을 수 있습니다.

 

사용자 interface를 더 작은 atom, molecule, organism으로 쪼개면 비즈니스 로직을 가지지 않게 할 수 있습니다. Hoc는 atomic 단계에서 필요한 데이터를 내려주고 api 호출들을 다룰 수 있습니다

 

가끔, orgnism, molecule에서 api 혹은 비즈니스 로직에 접근해야 할 수 있습니다. 당연한 일입니다. 하지만 컴포넌트 내부의 코드가 너무나도 복잡해진다면 무엇인가 잘못된 것입니다


결론

React 에서 Atomic 디자인을 따르는 것은 본질적으로 개발자들에게 컴포넌트를 단순하고 작게 쪼갤 수 있게 해줍니다.

이러한 단순성으로부터 우리는 더 복잡한 컴포넌트드로가 사용자 인터페이스를 구성하는 컨테이너 컴포넌트들을 만들 수 있습니다.

이러한 패턴을 따르는 것은 React 어플리케이션들을 다루기 쉽게 해줍니디ㅏ.

 

보너스: 파일들을 Atomic Design에 맞게 구성하세요

위의 방법론을 atoms/, molecules/, organisms/, templates/, pages/와 같이 파일을 구성하는데도 사용할 수 있습니다. 

start kit 중 Atomic React 는 좋은 예시입니다. Personally, I like a little bit of a mix of things and have written an article about folder structure in React, but its definitely an option for you 

 

 

 

 

 

출처 : https://medium.com/@wheeler.katia/thinking-about-react-atomically-608c865d2262

 

Thinking About React, Atomically ⚛

Utilizing Brad Frost’s Atomic Design principles to better architect React applications

medium.com

 

728x90
반응형

'Frontend > React' 카테고리의 다른 글

pnpm과 이모저모  (0) 2025.11.27
Redux persist와 migration  (0) 2025.11.27
React patterns 🤔  (0) 2023.06.06
React 공식문서 주요개념 살펴보기  (1) 2022.05.26
React what is JSX? (번역글) 🤔  (2) 2022.04.27
728x90
반응형

아래 출처의 글을 보고 번역 및 주요 내용을 정리한 글입니다. 

 

  • 어떻게 하면 다양한 경우에 적합한 재사용 가능한 컴포넌트를 build할 수 있을까?
  •  어떻게 하면 사용하기 쉽게 단순한 API와 컴포넌트를 만들 수 있을까?
  • 어떻게 하면 UI와 기능적으로 확장 가능한 컴포넌트를 만들 수 있을까??

에 대한 고민으로 이 글은 시작합니다.

 

5가지의 패턴을 제시하고 장점, 단점을 리스트업 해줍니다.

그리고 두개의 기준을 명시해 주었습니다.

  • 제어의 역전 : 컴포넌트를 사용하는 사람들에게 제공되는 유연성과 주도권의 정도
  • 구현의 복잡도 : 해당 패턴을 구현하는데 복잡도

1.  Compound Components Pattern

이 패턴은 prop drilling을 피하면서 선언적으로 컴포넌트를 구성할 수 있게 해줍니다. 이해가 쉬운 api와 관심사의 더 나은 분리와 함께, customizable 컴포넌트를 만들고 싶다면 이 패턴을 고려하세요

 

장점

* api의 복잡도 감소: 거대한 부모 컴포넌트에 props들을 묶어서 보내지 않고 자식 ui 컴포넌트에 prop drilling을 피할 수 있ㄷ습니다. 대신 Counter's의 자식들에 prop들이 매칭되므로 더 이해하기 쉽습니다

* 유연한 마크업 구조: 다양한 경우의 대응과 함께 좋은 ui 유연성을 제공하빈다. 예를 들면, Counter's의 자식들의 순서를 쉽게 변경하거나 어떻게 보일것인지를 결정할 수 있습니다.

* 관심사의 분리: 대부분의 로직은 Counter에 집중됩니다. 하나의 context(CounterProvider + useCounterContext)가 Counters' 자식들에게 상태(counter)와 handler(handleIncrement(), handleDecrement())를 다루기 위해 사용됩니다.

책임의 분배를 더 명확히 해줍니다.

단점

* 너무 높은 ui 유연성 : 이 정도의 유연성은 애초에 기대하지 않았던 상황을 초래할 수 있습니다(i.e: 원하지 않던 코드, Counter's 자식들의 잘못된 순서, 필수 자식들의 실종). 상황에 따라 너무 많은 유연성을 원하지 않을 수 있습니다.

 

* 비대해진 JSX: jsx의 크기를 증가시킵니다. eslint를 쓰거나 prettier를 쓰면 더더욱 그러할 것입니다. 하나의 컴포넌트에서는 미미해보일 수 있지만, 전체 큰 그림에서 보게되면 큰 차이가 있을 수 있습니다

기준

* 제어의 역전 : 1/4

* 구현의 복잡도 : 1/4

 

이 패턴을 사용하는 라이브러리

* React Bootstrap

* Reach ui

2. Control Props Pattern

* 이 패턴은 컴포넌트를 제어 컴포넌트로 만듭니다. 외부 상태는 컴포넌트의 기본 동작을 수정할 수 있는 로직을 넣을 수 있는 "single source of truth"로 동작합니다.

 

장점

* 더 많은 제어권: 개발자가 주요 상태를 제어할 수 있기 때문에, Counter의 동작에 직접 영향을 줄 수 있습니다.

단점

* 구현의 복잡도: 이전에는 하나의 통합된 부분으로 컴포넌트의 동작이 충분했습니다. 이제는 3개의 다른 부분이 존재 합니다.

 

기준

* 제어의 역전: 2/4

* 구현의 복잡도: 1/4

 

이 패턴을 사용하는 라이브러리

* Material UI

 

3. Custom hook Pattern

* 제어의 역전에서 나아가 보자: 주요 로직은 커스텀 훅 내부에 위치합니다. 이 훅은 몇몇 내부 로직으로 구성되어있고 개발자에게 좋은 제어권을 제공합니다.

 

장점

* 더 많은 제어권: 개발자는 Counter 동작을 수정하면서, useCounter와 Counter 사이에 커스텀 로직을 더 부여할 수 있습니다. 

 

 

단점 

* 구현의 복잡도: 로직 부분이 render 부분과 분리되었기 때문에 개발자는 두 군데를 신경써야 합니다. Counter가 어떻게 동작하는지 잘 이해하는 것이 이 패턴을 수행하기 위해 필수 입니다.

 

 

기준 

* 제어의 역전 : 2/4

* 구현의 복잡도 : 2/4

 

이 패턴을 사용하는 라이브러리

* React table

* React hook form

 

 

4. Props Getters Pattern

* Custom Hook Pattern은 더 나은 제어권을 제공하지만 로직을 재생성해야 하고 원래 hook의 props를 다루어야 하기 때문에 컴포넌트의 통합을 어렵게 합니다.

Props Getters Pattern 패턴은 이러한 복잡도를 다루기 위해 시도합니다. props들을 노출시키는 대신에 getters들을 제공합니다.

getter은 많은 prop들을 반환하는 함수 입니다. 의미있는 이름을 가지고 있고 jsx 요소에 어떤것이 대응하는지 명확히 할 수 있습니다.

 

장점

* 쉬운 사용: 복잡도는 숨겨져 있습니다. useCounter에서 제공하는 getter를 jsx 요소에 알맞게 연결하기만 하면 됩니다.

* 유연성: getter를 특별한 경우에 오버로딩 할 수 있습니다.

 

단점

* 가독성의 저하: getters는 컴포넌트를 통합하기 수월하게 하는 추상화를 제공해 주지만 좀 더 모호합니다. 개발자는 getter props들을 잘 이해하고 있어야 하고 영향 받는 로직들을 오버라이드 하기 위해 적절히 이해하고 있어야 합니다.

 

기준

* 제어의 역전: 3/4

* 구현 복잡도: 3/4

 

이 패턴을 사용하는 라이브러리

* React table

* Downshift

 

 

5. State reducer Pattern

제어의 역전에서 가장 발전된 패턴이빈다. 컴포넌트가 내부적으로 컴포넌트가 작동하는 방식을 변경할 수 있는 더 나은 방법을 제공합니다. Custom Hook Pattern과 유사하지만 hook을 위해 reducer를 제공해야 합니다. 이 reducer는 컴포넌트의 어떠한 내부 동작이라도 오버로드 할 수 있습니다.

 

장점

* 더 많은 제어권 : 복잡한 경우 state reducer의 사용이 개발자에게 제어권을 주기 가장 좋은 방법입니다. useCounter's의 모든 내부 동작은 밖에서 접근이 가능하고 override될 수 있습니다.

 

단점

* 구현의 복잡도: 이 패턴은 구현하기 가장 복잡합니다

* 가독성의 저하: reducer의 내부 동작이 변경될 수 있기 때문에 컴포넌트 내부 로직의 높은 이해가 필수 입니다.

 

기준 

* 제어의 역전: 4/4

* 구현 복잡도: 4/4

 

이 패턴을 사용하는 라이브러리

* Downshift

 

결론

* 5개의 React 패턴들을 통해 제어의 역전 측면에서 이점을 가지는 방법들을 살펴 보았습니다. 유연하고 확장가능한 컴포넌트를 만드는 강력한 방법들을 제공합니다.

그러나, "큰 힘에는 큰 책임이 따른다"라고 알고 있듯이, 개발자에게 더 많은 제어권이 주어지면, "plug and play" 방식에서 컴포넌트는 더 멀어질 수 있습니다. 이것이 적절한 패턴을 적절한 경우에 사용하여야 하는 이유입니다.

 

아래의 그림은 제어의 역전과 구현 복잡도에 따라 패턴을 분류한 그림입니다.

 

 

 

 


 

출처: https://javascript.plainenglish.io/5-advanced-react-patterns-a6b7624267a6

 

5 Advanced React Patterns

An overview of 5 modern advanced React patterns, including integration codes, pros and cons, and concrete usage within public libraries.

javascript.plainenglish.io

 

728x90
반응형
728x90
반응형

문득 setTimeout에 전달된 함수는 어떻게 에러처리를 해야하난 궁금해져서 이 문서를 작성합니다.

 

살펴볼 예시는 4가지 입니다.

  • try/catch
  • setTimeout with try/catch
  • Promise의 catch
  • async/await with try/catch

 


try/catch

 

try/catch는 예외상황을 다뤄야 할때 주로 사용합니다.

javascript가 런타임에서 에러가 나면 이후의 코드는 실행되지 않고 멈추어 버리기 때문에 예외처리는 중요합니다.

예외가 발생할 수 있는 곳에 보험을 들어서 이후의 동작을 수행할 수 있도록 하는 것이 중요하다고 생각합니다.

 

아래의 코드가 있습니다.

하나는 try/catch로 감싼것, 감싸지 않은 것입니다.

감싸지 않은 것에서는 에러가 발생하여 이후의 동작을 하지 않는 것을 볼 수 있습니다.

런타임에서 아래와 같은 상황이 발생하면 사용자는 불편함을 겪을 것입니다.

 

아래의 코드는 try/catch로 에러를 감싸주었고 이후의 코드가 실행되는 모습을 볼 수 있습니다.

 


setTimeout with try/catch

 

setTimeout과 같은 비동기 동작에서 try/catch를 사용하려면 어떻게 해야 할까요???

아래의 코드는 제가 의도한 대로 동작이 될까요?

try {
    setTimeout(() => {
        throw new Error('hihi')
        console.log('byebye')
    }, 500)
} catch(e) {
    console.log(e)
}

정답은 동작하지 않습니다.

 

태스크 큐에 존재하던 setTimeout의 콜백함수가 새로이 실행컨텍스트를 생성하여 실행이 되기 때문에 우리가 원하는대로 동작하지 않을까 싶습니다.

콜백함수 자체를 try/catch로 감싸주면 우리가 원하는대로 동작이 수행될 것입니다.

위의 사진은 우리가 원하는대로 에러를 catch하고 이후의 동작을 수행해 줍니다.

 


promise의 catch

 

비동기 동작을 지원해주는 promise에서는 catch를 이용하여 에러를 처리해줄 수 있습니다.

아래의 코드에서는 resolve를 통해 then을 실행시켜주었습니다.
then 내부에서 error를 반환하여 catch에서 처리해 줄 수 있도록 하였습니다.

아래와 같이 reject를 반환하여 바로 catch에서 에러 처리를 할 수 도 있습니다.

 


async/await with try/catch

 

es2017에서 등장한 async/await 입니다.

promise의 then chaining은 방대해지는 경우 코드의 가독성을 떨어뜨렸습니다.

이러한 점을 반영하여 async/awiat이 등장합니다.

 

promise -> generator -> async/await의 흐름으로 진행이 됩니다. 

generator에 대한 내용은 나중에 언급하겠습니다.

next와 yield를 통해 함수의 실행을 정지 시킬 수 있다가 핵심입니다.

 

async/await은 try/catch로 감싸주어 에러 처리를 할 수 있습니다.

a = () => new Promise((resolve, reject) => {
  reject('hihi')
})

(async () => {
    try {
        await a()
    } catch(e) {
        console.log(e)
    }
    console.log('byebye')
})()

아래는 실행된 모습입니다. 

await에서 발생한 에러를 try/catch문에서 처리해주고 이후의 동작을 수행하는 것을 볼 수 있습니다.

 

 

비동기 에러 처리 관련 질문을 받았을때 제대로 답변하지 못해 이렇게 정리하여 포스팅을 합니다.

728x90
반응형
728x90
반응형

3. 엘리먼트 렌더링

React에서 엘리먼트는 최소 단위 입니다.

이런 엘리먼트가 모여서 컴포넌트를 이루고 컴포넌트들이 모여서 프로덕트가 완성됩니다.

엘리먼트는 화면에 표시해줄 내용을 가지고 있습니다.

const element = <h1>Hello, world</h1>;

이러한 엘리먼트를 화면에 렌더링하려면 ReactDOM.render()를 이용하면 됩니다.

ReactDOM.render에는 엘리먼트와 루트 DOM 노드가 들어가게 됩니다.

<div id="root"></div>


const element = <h1>Hello, world</h1>
ReactDOM.render(element, document.getElementById('root'))

// ReactDOM.render(엘리먼트, 루트 DOM 요소)

React에서 엘리먼트는 불변객체 입니다. (엘리먼트가 생성된 이후에 자식이나 속성을 변경한다든가 하는 동작이 불가능!!!)

엘리먼트를 갱신하는 방법은 새로운 엘리먼트를 생성하고 이를 ReactDOM.render로 전달하는 방법 뿐입니다.

 

function tick() {
  const element = <h1>Hello, world</h1>
  
  ReactDOM.render(element, document.getElementById("root"))
}

setInterval(tick, 1000);

 

이렇게 매번 엘리먼트가 갱신이 되면 효율적으로 렌더링 하기 위하여

가상 DOM과 실제 DOM을 비교하여 변경된 부분만 실제 업데이트를 하게 됩니다.

가상 DOM의 재조정 알고리즘은 추후에 다뤄볼 수 있도록 하겠습니다.

아래 이미지는 엘리먼트가 매번 바뀌지만 전체가 변경되는 것이 아닌 내용이 변경된 텍스트 노드만 업데이트 되는 모습입니다.

 

 

 


4.  Component와 Props

props는 속성을 나타내는 데이터입니다.

컴포넌트는 함수형 컴포넌트와 클래스형 컴포넌트로 나뉘어 집니다.

const test = () => {
  return <h1>hihi</h1>;
}

위의 예시는 함수형 컴포넌트

class Test extends React.Component {
  render() {
    return <h1>hihi</h1>
  }
}

위의 예시는 클래스형 컴포넌트 입니다.

 

클래스형 컴포넌트는 훅의 등장 이후에는 널리 사용되지 않는걸로 알고 있습니다.

 

훅은 아래와 같은 이유로 등장을 하였습니다.  

컴포넌트에서 상태로직을 재사용하기 위해

클래스형 컴포넌트의 this는 혼잡을 가져왔고,

componentDidUpdate, componentDidMount와 같이 lifecycle에 중복되는 메서드들이 위치되어 지는 상황을 위해

 

아래의 예시부터는 함수형 컴포넌트를 기준으로 다루게 됩니다.

 

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>
}

const element = <Welcome name="Sara" />
ReactDOM.render(
  element,
  document.getElementById('root')
)

 

1. ReactDOM.render에서 Welcome 컴포넌트를 호출합니다.

2. Welcome 컴포넌트에 props로 name이 넘어갑니다.

3. Welcome 컴포넌트는 <h1>Hello, sara</h1>를 호출합니다.

4. ReactDOM은 <h1>Hello, Sara</h1>엘리먼트와 일치하도록 DOM을 효율적으로 업데이트 합니다(가상돔 개념)

 

 

!!! React 에서 커포넌트 이름은 항상 대문자여야 합니다. 소문자로 작성하게 되면 html 태그로 인식하게 됩니다.

 

 

컴포넌트 합성

여러 컴포넌트를 합성하여 하나의 컴포넌트를 만들 수 있습니다

function Welcome (props) {
  return <h1>hihi</h1>
}

function App() {
  return (
    <Welcome />
    <Welcome />
    <Welcome />
  )
}

 

컴포넌트 추출

function Comment(props) {
  return (
    <div className="Comment">
      <div className="UserInfo">
        <img className="Avatar"
          src={props.author.avatarUrl}
          alt={props.author.name}
        />
        <div className="UserInfo-name">
          {props.author.name}
        </div>
      </div>
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

위의 컴포넌트를 아래의 여러 컴포넌트로 추출할 수 있습니다.

 

function Comment(props) {
  return (
    <div className="Comment">
      <UserInfo author={props.author} />
      <div className="Comment-text">
        {props.text}
      </div>
      <div className="Comment-date">
        {formatDate(props.date)}
      </div>
    </div>
  );
}

function UserInfo(props){
  <div className="UserInfo">
  	<Avatar user={props.author} />
    <div className="UserInfo-name">
      {props.author.name}
    </div>
  </div>
}

function Avatar(props) {
	<img className="Avatar"
      src={props.user.avatarUrl}
      alt={props.user.name}
    />
}

 

!!!! props의 이름은 사용될 context가 아닌 컴포넌트 자체의 관점에서 짓는 것을 권장합니다!!

=> 컴포넌트는 재사용될 수 있으므로 컴포넌트 자체의 관점에서 바라보는 것이 좋다고 생각합니다.

context의 관점에서 props의 이름을 설정하면 재사용 했을때 새로운 context의 이해관계와 충돌이 날 수 있다고 생각합니다.

 

Props는 읽기 전용입니다!

모든 react 컴포넌트는 자신의 Props를 다룰 때 반드시 순수 함수처럼 동작해야 합니다!!

순수함수란 동일한 입력값에 항상 동일한 결과를 반환하는 특성을 가지고 있습니다.

 


5. State와 생명주기

function Clock(props) {
  return (
    <div>
      <h1>Hello, World</h1>
      <h2>It is {props.date.toLocaleTimeString()}</h2>
    </div>
  )
}

function tick() {
  ReactDOM.render(
    <Clock date={new Date()} />,
    document.getElemnetById("root")
  )
}

setInterval(tick, 1000)

 

props를 외부에서 주입하지 않고 state에서 new Date() 값을 가지게 하였습니다.

저는 위의 함수를 아래와 같은 함수형 컴포넌트로 변경하였습니다.

 

function Clock() {
  const [date, setDate] = useState(new Date())
  useEffect(() => {
    const tick = setInterval(() => setDate(new Date()), 1000)  
    return () => clearInterval(tick)
  }, []);

  return (
    <div>
      <h1>Hello, World</h1>
      <h2>It is {date.toLocaleTimeString()}</h2>
    </div>
  );
}


ReactDOM.render(<Clock />, document.getElementById("root"));

 

위의 date는 커스텀 훅으로 뺄 수도 있을 것 같습니다! 재사용이 필요하다면 이동하여 위치 시킬 수 있습니다.

state는 props와 유사하지만, 비공개이며 컴포넌트에 의해 완전히 제어됩니다.

 


6. 이벤트 처리하기

react에서 이벤트는 소문자 대신 카멜케이스를 사용합니다.

<button onClick={activateLasers}>
  Activate Lasers
</button>

 

새로 알게된 내용 

=> react에서는 Return false를 반환해도 기본 동작을 반환할 수 없습니다.

html에서는 가능합니다.

<form onsubmit="console.log('You clicked submit.'); return false">
  <button type="submit">Submit</button>
</form>
const Form = () => {
  function handleSubmit(e) {
    e.preventDefault();
    console.log('You Clicked submit.');
  }
  
  return (
    <form onSubmit={handleSubmit}>
    	<button type="submit">Submit</button>
    </form>
  )

}

 


7. 조건부 렌더링

React에서는 조건부 렌더링을 수행할 수 있습니다.

상황에 따라 로그인 컴포넌트나 로그아웃 컴포넌트를 리턴해 줄 수 있습니다.

 

아래와 같은 경우 Props로 전달받은 isLogin에 따라 LogoutComponent, LoginComponent를 리턴해 줄 수 있습니다.

const Component = ({ isLogin }) => {   
   if(isLogin) {
     return <LogoutComponent />
   }
   
   return <LoginComponent />
}

 

 

저는 주로 Early return을 사용하거나 return문 내에서 논리연산자를 활용하여 컴포넌트를 뱉어냅니다.

const Component = ({ isLogin }) => {      
   return (
   	<div>
    	{isLogin && <LogoutComponent />}
        {!isLogin && <LoginComponent />}
    </div>
   )
}

 

삼항연산자도 사용할 수 있습니다.

const Component = ({ isLogin }) => {      
   return (
   	<div>
    	{isLogin ? <LogoutComponent /> : <LoginComponent />}
    </div>
   )
}

 


8. 리스트와 key

리액트에서는 리스트 컴포넌트를 표현할 수 있습니다

const numbers = [1,2,3,4,5]
const Items = () => {
  return (
  {numbers.map(number => <div>{number}</div>)}
  )
}


ReactDOM.render(<Items />, document.getElementById('root'))

위의 코드는 리액트에서 리스트를 표현하는 대표적인 방식입니다.

 

이때, key를 넣어주어야 합니다.

리액트에서는 같은 레벨의 자식들 간에는 Key값을 이용하여 재조정 해야할지 말지를 판단하기 때문입니다.

const numbers = [1,2,3,4,5]
const Items = () => {
  return (
  {numbers.map(number => <div key={number.toString()}>{number}</div>)}
  )
}


ReactDOM.render(<Items />, document.getElementById('root'))

 

같은 형제 노드들에서 값이 변하지 않았다면 key 값으로 판단하여 리렌더링을 하지 않습니다.

가령 index를 key값으로 사용하는 경우가 있습니다.

이러한 경우에는 비효율적인 리렌더링이 발생할 수 있습니다.

 

아래의 경우는 맨 끝에 5가 포함된 모습입니다.

이러한 경우에는 1,2,3의 리렌더링이 발생하지 않습니다.

React에서는 Key를 통해 항목의 순서가 변경되지 않았다는 것을 알 수 있으니까요

<div key={1}>1</div>
<div key={2}>2</div>
<div key={3}>3</div>

<div key={1}>1</div>
<div key={2}>2</div>
<div key={3}>3</div>
<div key={4}>5</div>

 

아래의 경우는 4가 1앞에 추가된 모습입니다.

key값은 바뀌지 않았지만 값의 변화가 생긴것을 볼 수 있습니다.

모든 요소들의 Key값이 변경된 모습을 확인할 수 있습니다.

react에서는 같은 자식레벨에서 key를 통해 기존 트리와 이후 트리가 일치하는 지 확인합니다.

 

인덱스를 key로 사용하면 항목의 순서가 바뀌었을때 key값이 변경될 수 도 있습니다.

input을 체크했는데 key값이 변경되면 label만 변경된다 거나 하는 의도치 않은 상황을 맞이할 수 도 있습니다.

(React에서는 동일한 key값에 같은 DOM 요소를 보여줍니다)

<div key={1}>1</div>
<div key={2}>2</div>
<div key={3}>3</div>

<div key={1}>4</div>
<div key={2}>1</div>
<div key={3}>2</div>
<div key={4}>3</div>

 


9. 폼

html에서 input, select, textarea와 같은 폼 엘리먼트는 사용자의 입력을 기반으로 자신의 state를 가지게 됩니다.

React에서는 컴포넌트에서 state를 가지게 됩니다. state를 input, select, textarea에 value 속성으로 전달함으로써 제어컴포넌트로 관리할 수 있습니다.

값이 바뀔때마다 컴포넌트가 리렌더링 된다는 (상태가 변경되기 때문에) 단점이 있지만 편리함때문에 비제어 컴포넌트와 잘 고려하여 사용을 하는게 좋을듯 합니다.

cpu의 성능까지 고려하면 더 좋을듯 하구여

 

return (
  <form onSubmit={this.handleSubmit}>
    <label>
      Name:
      <input type="text" value={this.state.value} onChange={this.handleChange} />
    </label>
    <input type="submit" value="Submit" />
  </form>
);

return (
  <form onSubmit={this.handleSubmit}>
    <label>
      Essay:
      <textarea value={this.state.value} onChange={this.handleChange} />
    </label>
    <input type="submit" value="Submit" />
  </form>
);
    

return (
  <form onSubmit={this.handleSubmit}>
    <label>
      Pick your favorite flavor:
      <select value={this.state.value} onChange={this.handleChange}>
        <option value="grapefruit">Grapefruit</option>
        <option value="lime">Lime</option>
        <option value="coconut">Coconut</option>
        <option value="mango">Mango</option>
      </select>
    </label>
    <input type="submit" value="Submit" />
  </form>
);

 


10. State 끌어올리기

 

state 끌어올리기는 컴포넌트 내부에서 관리되던 상태를 공통으로 사용해야 하는 컴포넌트와의 가장 가까운 공통 부모로 이동시키는 것을 의미합니다.

 

리액트 공식문서에서는 온도를 화씨와 섭씨로 변경하여 보여주는 부분을 예시로 보여주었습니다.

class Calculator extends React.Component {
  constructor(props) {
    super(props);
    this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
    this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
    this.state = {temperature: '', scale: 'c'};
  }

  handleCelsiusChange(temperature) {
    this.setState({scale: 'c', temperature});
  }

  handleFahrenheitChange(temperature) {
    this.setState({scale: 'f', temperature});
  }

  render() {
    const scale = this.state.scale;
    const temperature = this.state.temperature;
    const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
    const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;

    return (
      <div>
        <TemperatureInput
          scale="c"
          temperature={celsius}
          onTemperatureChange={this.handleCelsiusChange} />
        <TemperatureInput
          scale="f"
          temperature={fahrenheit}
          onTemperatureChange={this.handleFahrenheitChange} />
        <BoilingVerdict
          celsius={parseFloat(celsius)} />
      </div>
    );
  }
}

 

 

이는 제가 의식하고 사용하려고 하는 방식중 하나입니다.

제가 상태를 관리하는 원칙은 다음과 같습니다. 

이러한 근거는 상태는 이용되는 컴포넌트와 가까운 위치에 정의되어 있어야 한다는 기조에 근거합니다.

1. 상태는 사용하는 컴포넌트에서 관리

2. 같은 상태를 공유해야하는 컴포넌트와 가장 가까운 부모 컴포넌트에 상태를 이동 (상태 끌어올리기 사용)

3. 거리가 너무 멀다면 contextAPI 적용 

4. Redux나 Recoil 적용 검토

 

상태와 컴포넌트간의 거리가 너무 멀다면 props drilling이 발생하고 코드의 흐름을 파악하기 어려워 집니다.

또한 불필요한 리렌더링이 발생할 수 있으므로 위와 같은 방식을 저는 선호합니다.

그중 첫단계인 상태 끌어올리기에 관한 내용이 10장 이었습니다.

728x90
반응형
728x90
반응형

아래 출처의 글을 정리하고 약간의 생각을 가미하려 합니다. 허락해주신 테오님께 갑사합니다 ㅎㅎ

 

MVC패턴 MVVM패턴 등등이 많이 언급되고 사용된다 하지만 저는 MVC 패턴만 간략하게 이해가 되고 다른 것들은 쉽게 이해가 되지 않았습니다. 그러던 와중 좋은 글을 읽게 되었고 저자의 허락을 얻어 저의 언어로 정리하고 인사이트를 작성해 보고자 합니다.

 

1. 아키텍쳐란 무엇일까요?
2. 웹 프론트엔드 아키텍쳐 이야기
3. 현대 웹 프론트엔드의 아키텍쳐 방향성

 


1. 아키텍쳐란 무엇일까요?

아키텍처란 영어로는 '건축학'이라는 뜻입니다.

 

이 단어가 개발분야로 유입되면 조금 다른 뜻을 가지게 됩니다.

아래 사진들은 옷장에 대한 사진들 입니다.

두개의 옷장은 전부 같은 양의 옷을 보관하고 있다고 가정하겠습니다.

그리고 옷은 하나의 기능, 즉 코드로 작성된 함수라고 하겠습니다.

두 옷장은 같은 동작을 수행합니다. 같은 양의 옷이 보관되어있고 양말을 꺼내고 싶다(input) 양말이 나온다(output)과 같은 기능을 수행할 수 있습니다.

 

하지만 a 양말을 버려야 한다. b 티셔츠를 꺼내서 당근마켓에 올려야 한다. 라는 요청사항이 들어왔을때 어떤 옷장이 동작을 수행하기 빠를까요?

 

아키텍쳐는 이러한 옷장과 비슷하다고 합니다. 물론 옷장은 옷을 보관하는 용도만 수행하지만 아키텍쳐는 아닙니다. 각 모듈들은 서로 데이터를 주고 받고 영향을 끼칠 수도 있습니다.

 

마틴 파울러의 유투브 발표영상을 보면 https://www.youtube.com/watch?v=4E1BHTvhB7Y 경제학적인 관점이 등장합니다. 

소프트웨어 아키텍처는 아쉽게도 외부에 드러나지 않습니다.

같은 기능을 수행하는 a, b 프로그램이 있습니다.
a 프로그램은 잘 설계된 아키텍처를 가지고 있습니다. 가격은 200달러 입니다.
b 프로그램은 좋지 못한 아키텍처를 가지고 있습니다. 가격은 100달러 입니다.

프로그램을 구매해야 하는 사람의 입장에서 보면 a, b중 어떤 프로그램을 구매할까요?
b를 구매하는 사람이 많지 않을까 싶습니다.
우리는 물건을 구매할때 비용과 품질의 트레이드오프가 일어납니다. 아반떼와 포르쉐 카이옌의 가격은 다르고 더 좋은 품질을 비싼 가격에 기대하게 됩니다.
프로그램의 내부 아키텍처는 외부에 드러나지 않습니다. 그래서 아키텍처는 간과되기 쉽습니다.
개발자들이 아키텍처의 설계에 아무리 공을 들여도 비용을 관리하는 입장에서는 같은 동작이 수행되니 프로그램에서의 비용과 품질의 트레이드 오프를 이해할 수가 없습니다

프로그램에서의 비용과 품질의 트레이드 오프는 장기적인 관점에서 바라봐야 합니다.
잘 설계된 아키텍처는 기능의 추가와 기존 기능의 수정이 편리합니다.

 

상단의 두 개 옷장에서 같은 옷을 꺼내야 하고 상의와 겉옷 칸의 위치를 바꿔야 한다고 생각해보세요. 

어떤 옷장이 수월할 까요?

 

그래서 좋은 아키텍처를 가진 프로그램은 장기적으로 유지 보수와 기능의 추가가 상대적으로 매우매우 용이하여 결과적으로는 비용을 줄여줍니다.

 

하지만 초기 비용은 증가합니다. 옷이 적을때는 옷장을 더럽게 해도 저의 요청사항들(같은 옷을 꺼내기, 상의와 겉옷 칸의 위치를 바꾸기)들을 수행하기 쉽습니다. 오히려 더 빠를 수도 있습니다. 그래서 프로그램의 규모, 예산등이 고려되어야 하지 않을까 싶습니다. 그래도 경험상 무조건 좋은 아키텍쳐를 설계하고 가는 것이 좋지 않을까 생각이 듭니다

 

 

2. 웹 프론트엔드 아키텍쳐 이야기

아키텍처는 좋은 구조를 만드는 것이고 좋은 구조를 위해서는 좋은 분류가 필요합니다


MVC 아키텍처

Model + View + Controller 의 조합입니다.

 

Model(모델) : 

  화면에 보여줄 데이터를 담당하는 영역입니다.

  Model의 정의는 주어진 환경에 따라 다를수 있습니다.

  javascript의 Object, 서버의 DB, 서버에서 API로 요청한 데이터 일 수도 있습니다.

 

View(화면) :

  실제 사용자에게 보여지는 화면입니다.

 

Controller(컨트롤러) : 

   모델과 View사이에서 중간 역할을 합니다. 

   Rails 에서는 
      컨트롤러에서 사용자의 요청을 받습니다.
      요청에 필요한 데이터를 모델에서 가져옵니다.
      가져온 데이터를 화면에 적용하여 사용자의 요청에 맞는 화면을 그려냅니다.
      그려진 화면을 사용자에게 보여줍니다.
   와 같이 동작합니다.

    

이렇게 MVC를 나눈 이유는

1. 화면을 다루는 문제, 데이터를 다루는 문제는 서로의 관심사가 다릅니다. 두개의 문제가 동일한 곳에서 관리되어 진다면 기능의 수정이 필요할때 개발자는 코드로의 모험을 떠나야 할 겁니다.

2. Model과 View의 의존관계를 최소화 함으로써 화면의 수정은 데이터의 수정에, 데이터의 수정은 화면의 수정에 영향을 미치지 않을 수 있습니다.

 

 

가장 이해가 되지 않던 부분이 프론트엔드에서의 Model 부분이었습니다. 

하지만 해당 블로그에서 시간의 흐름에 맞게 Model, View, Controller의 정의에 대한 변화 또한 친절하게 설명해주어 많이 와닿았습니다.

 

초창기 웹 서비스 -> jQuery 시절 -> MVVM 아키텍쳐 (angular, react, vue) -> 컴포넌트 그리고 Container-Presenter 패턴 -> Flux  패턴, Redux -> MVI 패턴 의 흐름으로 이어집니다.

 

흐름을 이해하는 것이 뭔가를 이해하는 데 있어 매우 유용한 것 같습니다.

Flux 패턴은 기존의 어떤 문제를 해결하려 했는지, 기존의 문제는 어떤걸로 인하여 발생하였는지를 알수 있다면 다음 문제를 예측하고 기존의 문제와 같은 문제를 발생하는 것을 피해갈 수 있습니다.
"역사를 잊은 민족에게 미래는 없다" 와 같은 문장도 같은 맥락에 있지 않을까 싶습니다

 

초창기 웹 서비스 

 

아키텍처의 잣대와 범주는 언어마다 각 환경마다 다르다고 합니다. 

그래서 저는 현대의 아키텍처에 기존에 가지고 있던 시야와 뜻을 접목했었고 잘 이해를 못했었습니다(지금은 공부중입니다!)

Model : 데이터 베이스
View : Html, css. js를 포함한 클라이언트 영역
Controller :  Model과 View 가운데서 라우터를 통해 데이터를 처리하고 새로운 HTML을 만들어서 보여주는 백엔드 영역


jQuery 시절의 MVC 아키텍쳐

프론트엔드에 ajax가 등장하였습니다. Ajax(Asynchronous javascript and xml)의 뜻으로 간단하게 js에서 페이지의 새로고침 없이 XMLHttpRequest 객체를 통해 리소스를 요청할 수 있는 것을 의미합니다

 

초창기 시절과 MVC의 개념이 조금씩 바뀌게 됩니다

Model : ajax로 부터 받는 데이터
View :  HTML과 CSS로 만들어지는 화면
Controller : javascript가 중간에서 서버의 데이터를 받아 화면을 바꾸고 이벤트를 처리해서 서버에 데이터를 전달하는 컨트롤러의 역할을 수행하게 됩니다. => javascript가 서버에 데이터를 요청하고 받아온 데이터를 화면에 그려줍니다.

 

아래는 jquery에서 소개하는 간략한 소개에 나오는 주요개념 들입니다.

컨트롤러의 역할을 수행하기 위해 모델에 요청을 보내고 Dom을 탐색하고 조작하는 기능이 주요개념에 포함되어 있음을 확인할 수 있습니다

* Dom Traversal and Manipulation
* Event Handling
* Ajax 

 

당시 가장 중요한 패러다임은 관점의 분리로 Model과 View의 종속성을 최대한 분리하는 원칙으로  HTML과 jQuery를 따로 관리하는 것이었다고 합니다. 

 

 

MVVM 아키텍쳐 - angular, react, vue

jquery로 작업을 하다보니 같은 작업이 계속 반복되는 것을 발견합니다. 데이터를 수정하고 이벤트를 연결하고 수정하는 과정이 계속 반복된다는 것입니다.

 

서버에서는 Template library등을 자주 사용했었는데 {{}}와 같은 치환자가 사용이 가능한 것은 html이 전체적으로 렌더링이 가능하기 때문이었습니다. 이를 통해 선언적으로 개발이 가능했었습니다. 하지만 jQuery를 사용할때는 전체 렌더링을 매번 하는것이 아닌 필요한 곳만 변경을 찾아서 수정해 주어야 했습니다. 

 

이러한 불편함이 계속되던 중 angular 가 등장합니다.

 

angular에서는 템플릿과 바인딩 이라는 중요한 개념이 등장합니다. (앵귤러를 접해보지 않아 잘 모르겠습니다...)

Model이 변하면 View를 수정하고 View에서 이벤트를 받아서 Model을 변경한다는 Controller의 역할은 그대로 인데 이를 구현하는 방식이 jQuery와 같은 DOM 조작에서 템플릿과 바인딩을 통한 선언적인 방법으로 변하게 됩니다.

 

이제 코드는 Dom 조작을 직접 하지 않고 A를 B로 바꾸겠다고 선언을 합니다. 그러면 프레임워크에서 이를 처리해줍니다.

 

이제 View를 그리는 Model만 다루게 되었다는 의미인 ViewModel 이라고 부르며 이 방식을 MVVM이라 합니다.

React, Vue, Angular2, Svelte등 어떤 방식의 템플릿과 바인딩 문법을 쓰느냐 방식만 다를 뿐 MVVM이라는 아키텍처는 그대로 유지 됩니다.

 

MVC에서 MVVM으로 오면서 달라진 부분

* 컨트롤러의 반복적인 기능이 선언 방식으로 바뀜
* Model과 View의 관점을 분리하려 하지 않고 하나의 템플릿으로 관리하는 방식으로 발전(기존에는 class나 id 등으로 간접적으로 HTML에 접근, 그러나 이제는 직접적으로 HTML에 접근, React의 ref가 HTML에 직접 접근하기 위한 대표적 요소)

 

컴포넌트 그리고 Container-Presenter 패턴

 

MVVM은 웹의 DOM API를 알지 못하더라도 비즈니스 로직에만 집중하면 금방 서비스를 만들어 줄 수 있게 해주었습니다. 이는 생산성의 향상을 불러옵니다.

 

이제는 Page안에 여러가지 모듈이 존재하게 되고 Modal이나 여러 화면들이 하나의 화면을 이룰 수 가 있습니다.

그래서 MVVM이 화면 단위가 아니라 조금 더 작은 컴포넌트로 분리되어 이를 조립하는 방식이 됩니다.(angular의 구현 방식이 궁금하네요)

 

이 방식이 Component 패턴입니다. 컴포넌트 패턴에 비즈니스 로직이 들어가게 되면 컴포넌트의 재사용은 어렵습니다.

 

그래서 컴포넌트 에서 비즈니스 로직을 분리해야 했습니다.

이때 등장한 것이 Container/Presetaional 컴포넌트 입니다.

모든 상태/비즈니스 로직은 Container에서 Presentational 컴포넌트는 props를 뿌려주기만 합니다. 

이렇게 서로 관심사가 분리되어 있으므로 여러 컴포넌트에서 입맛에 맞는것을 가져다 쓰면 됩니다.

 

 

이 방식에서 하나의 문제가 발생했습니다.

상태의 관리와 상태를 보여주는 곳이 거리가 너무 멀면 중간에 위치한 컴포넌트 들이 props로 전달을 해주어야 했습니다. 상태가 변하면 중간에 위치한 컴포넌트 들도 리렌더링이 되어 불필요한 렌더링이 발생하게 되는 것입니다.

 

FLUX 패턴과 Redux

FLUX 패턴은 mvc 패턴에서 벗어나 단방향 아키텍처를 만들자는 아이디어에서 시작합니다. 

 

제가 기존에 이해했던 mvc 패턴의 문제는 하나의 view에서의 변화는 여러 model의 변화를 일으키고, 하나의 model에서의 변화는 여러 view의 변화를 일으킨 다는 것에 있다고 알고 있었습니다. 

이렇게 되면 변화를 예측하기가 힘들어지고 제어하기는 더 어려워진다는 것입니다.

 

블로그에서 언급된 내용은 기존의 mvc의 경계에 대한 이야기 라고 하였습니다.

같은 데이터를 공유하는 과정에서 props를 통해서 데이터를 전달하는 문제들로 하여금 Model의 관리가 파편화 되는 문제가 발생하였다고 합니다.

위의 경우 count 상태는 루트 컴포넌트에 위치하고 handler와 함께 props drilling을 통해 말미의 컴포넌트 들에 전부 전달해주어야 합니다.

 

그래서 단일 흐름의 필요성이 대두됩니다.

 

기존의 컴포넌트인 View에서 action을 발생시키고 이를 감지하던 dispatcher에서는 action의 종류에 맞게 store의 데이터를 변화 시킵니다.

store의 데이터를 참조하고 있던 view에서는 데이터가 변경됬음을 감지하고 관련 컴포넌트는 리렌더링 됩니다.

 

Redux

reducer + flux 의 합성어 입니다.

상태관리의 대표 적인 선두 주자 입니다.

제가 text로 설명한 dispatcher, action, store의 동작이 위의 그림으로 잘 설명이 됩니다.

 

다시한번 정리하자면 

Flux 패턴은 view를 기존의 MVC 컴포넌트 관점이 아닌 하나의 큰 View를 의미하고 View에서는 Dispatch를 통해 Action을 전달합니다.

Action은 Reducer를 통해서 Data를 변경해주고 Store에 보관합니다. 

Store에 있는 데이터는 View에 전달이 되어 사용자에게 보여집니다.

 

상태관리 => 기존의 컴포넌트 단위의 MVC 개념에서 비즈니스 로직과 View를 분리하면서 이 분리된 개념을 상태관리라고 부르게 됩니다

 

MVC에서 FLUX로 오면서 달라진 부분
 
* 공통적으로 사용되는 비즈니스 로직의 Layer와 View의 Layer를 완전히 분리하여 상태관리라는 방식으로 관리합니다.
* 각각의 독립된 컴포넌트가 아닌 하나의 거대한 View영역으로 간주합니다.
* 둘 사이의 관계는 Action과 Reducer라는 인터페이스로 분리하며 Controller는 이제 양방향이 아니라 단방향으로 Cycle을 이루도록 설계합니다.

 

FLUX 패턴은 본격적으로 상태관리 라이브러리가 등장하게 된 중요한 패턴임을 이 블로그를 읽고 알게 되었습니다.

 

하지만 Flux 패턴은 쉽지 않습니다.

또한 Redux를 사용하다 보면 action, reducer, saga등의 코드를 작성해야 하는 양이 너무 많았습니다.

redux-toolkit의 createSlice를 이용하면 함수 하나에 action, actionCreator, reducer, saga를 모두 이용할 수 있으므로 코드양이 줄어 드는 것을 확인할 수 있습니다.

 

Observer-Observable Pattern

 

초창기

 

props drilling 문제를 해결하기 위해 Observer-Observable Pattern 방식이 등장합니다.

Flux에서의 Dispatch와 Action을 배제하고 값이 바뀌면 모두에게 전달하는 방식입니다

(이 패턴에 대해서는 따로 공부가 필요할 것 같습니다)

 

초창기 Mobx에서 이 방식을 기반으로 하였다고합니다.

(graphql을 보면 subscribe를 할 수 있고 만약 eventBus에 event가 발생하면 subscribe 하고 있던 곳으로 effect을 주는 방식을 사용할 수 있는 것으로 알고 있습니다. amplify/dynamodb)

 

 

아래는 Angulart에서 Rxjs를 받아들이고 Flux 패턴과 결합한 상태관리 입니다.

 

 

 


3. 현대 웹 프론트엔드의 아키텍쳐 방향성

프론트엔드 프레임워크의 발전 서사는 아래의 순서로 알고 있습니다
jQuery -> 앵귤러js -> React -> 앵귤러2 -> vue, svelte

 

Context와 hook

 

React에서 context는 존재했었지만 hook의 등장전까지 공식문서에서 사용을 지양하라고 했었던 걸로 알고 있습니다. 

기존에는 사용이 복잡했었고 useContext 훅의 등장으로 그 사용이 간소화 된걸로 알고 있습니다. 

이제는 Redux를 대체해서 사용하려는 움직임도 있다고 알고 있습니다.

 

저는 이러한 원칙을 가지고 있습니다.

전역상태의 추가는 최대한 지양, 

상태의 선언 및 관리는 사용되는 컴포넌트에 최대한 가까이 위치 

가까운 거리라면 우선 상태 끌어올리기를 시도

상태 끌어올리기로 해결이 안되고 상태의 핸들러가 부모 컴포넌트의 외부에서 사용이 되어지지 않는다면 합성 컴포넌트 사용

부모 컴포넌트의 외부에서도 사용되야 한다면 context API의 사용을 검토합니다.

 

그 외 상태 간의 거리가 너무 멀거나 로그인 상태, 다국어 처리, 다크 모드등에는 전역 상태로 추가를 하여 관리를 하려 합니다.

또한 서버 상태는 react-query를 사용하여 관리를 맡기고 있습니다.

useState 내부에서 비용이 비싼 함수의 결과물을 상태로 지정해야 한다면 지연 초기화를 사용하여 컴포넌트가 마운트 되었을 시에만 함수가 실행하게 하여 값을 상태에 지정해 줄 수도 있습니다

 

개선 방향

* View와 Model은 분리한다.
* 프레임워크에서 Props Drilling 문제를 막는 방법을 제공하자

 

 

Atomic 패턴  (Recoil, Svelte Store, Vue Composition,  jotai)

(아쉽게도 제가 Recoil외에는 사용해보지 않아서 저들은 다음에 직접 사용해봐야 좀 더 와닿을것 같습니다)

 

View, Store의 분리는 그대로 가져가되 Action~Dispatcher~Store 의 복잡한 구조를 사용하지 않기 위해 만들어진 방법입니다.

Recoil은 훅의 쓰임새와 유사하게 사용이 가능하고 atom, selector등을 이용하여 전역 데이터를 관리, 커스텀 하기 용이합니다. 

Recoil 홍보대사는 아니지만 (아직 버전이 0. 대라 사용하지 않는 분도 있는 걸로 알고있습니다) 페이스북에서 관리하는 one-party library로 향후 리액트의 변화도 가장 빠르게 대응할 수 있지 않을까 싶은 개인적인 생각입니다.

 

개선방향

* View와 Model은 분리한다.
* 중간의 과정은 자율에 맡기고 간단하게 Model에 접근하는 법만 제공하자!!!!!
* 동기화, 동시성 처리가 중요

 

React-Query - MVC의 확대

서버 상태를 관리하기 위해 나온 대표적인 라이브러리 입니다. 

공식문서에도 서버상태를 관리하기 좋다고 나와있습니다.
서버 상태는 다음과 같은 특성을 가지고 있습니다. 
   1. 사용하는 쪽에서는 서버 상태가 무조건 존재한다는 가정이 있어야 합니다.
   2. a시점 에서의 상태와 b시점 에서의 상태가 다를 수 있습니다. 시점에 따라 상태의 stale 상태가 달라집니다. 

 

React-Query 에서는 고전적인 ajax의 데이터를 Model로 간주한다고 합니다.

프론트엔드 개발은 서버 데이터를 CRUD하고 시각으로 그리는 것에 중점이 되어 있는데 FLUX나 Atomic은 너무 복잡한 방법이라는 것입니다.

 

React Query는

* 서버와의 fetch 영역을 Model로 간주
* View는 React
* Controller는 query와 mutation이라는 2개의 인터페이스를 통해서 서버 데이터의 상태를 관리하고 캐싱, 동기화, refetch등을 관리하는 역할

 

블로그에서 서버로부터 불러온 데이터를 Model로 간주 한다는 것이 처음엔 정말 이해가 안갔지만 그냥 받아들이자라고 생각한 뒤에는 점차 체화 되는것 같습니다.

기술의 도입과 기반을 MVC, MVVM과 같은 패턴으로 바라보는것이 흥미로웠습니다.

저도 언젠가 저자와 같은 시야를 가질 수 있었으면 좋겠습니다.


 

아래 내용부터는 저자의 원글 그대로를 가져왔습니다.

제가 하고 싶은 얘기가 아니라 저자의 생각이여서 따로 색칠을 하지 않았습니다

 

 

최근 아키텍쳐의 방향성

 

MVI라는 워딩은 웹 프론트에서는 잘 쓰이지는 않지만 이 이미지가 제가 생각하는 최근 아키텍쳐와 가장 닮아있는 것 같아서 가지고 와봤습니다.

* 단방향 아키텍쳐
* 선언적 프로그래밍을 통한 Controller
* 뷰와 비지니스 로직의 분리(상태 관리)
* 반응형 프로그래밍
* 서버와의 연동을 Controller로 간주하는 움직임

 

사실 이름은 중요하지 않다. MV*

 

MVC, MVVM, FLUX, MVI, Atomic, Observable...

 

현재 쓰고 있는 라이브러리들이 저런 아키텍쳐에 딱 맞게 떨어지지는 않습니다. 아키텍쳐는 비슷한 것들을 묶어서 기존과는 다른 이름을 붙이면 우리가 개념의 범주를 만들기 좋게 하기 위한 일종의 마인드 셋입니다.

 

어떤 이름들은 라이브러리가 출시가 되고 나서 설명을 하기 위해서 만들어졌고, 어떤 이름들은 전략적으로 이전과는 다른 차별화를 강조하기 위해서 이름을 새로 짓기도 합니다.

 

 

 

앵귤러는 아예 MVW(Model-View-Whatever) 혹은 MV*로 불러달라고 했습니다. 뭐가 되었든 Model과 View 사이에서 무언가를 할테니까요. 

 

각 아키텍쳐의 이름보다는 웹 프론트엔드의 막연히라도 전반적인 아키텍쳐의 구성과 변화의 흐름을 이해해서 아키텍쳐의 본래 이유였던 지속적으로 잘 관리되는 코드를 짜기 위해서 지금 쓰는 아키텍쳐에 맞는 방식으로 더 코드를 잘 작성할 수 있는 기반 지식이 되었으면 합니다.

 

불편함을 느끼자!

 

이러한 패러다임의 확장과 개선이 있기 까지는 많은 시간이 걸립니다. 이러한 개선의 시작은 바로 현재의 개발방식에서 불편함을 느끼는 것입니다. 

 

서두에서 언급했던 아키텍쳐의 시작은 불편한것을 찾고 하지 말아야할 규칙을 찾는 것부터 시작합니다. 그리고 그 것을 개선하는 방법을 찾음으로써 새로운 패러다임이 생겨나게 됩니다. -> !!!!!!

 

내가 당장 새로운 라이브러리를 개발할 수는 없겠지만 언제나 이 불편함을 주시하고 있어야 앞으로 새로운 라이브러리가 나왔을때 그것이 개선이 되었는지 아니면 새롭지 않은 기존 라이브러리의 아류인지 판단할 수 있습니다.

 

웹은 언제나 무서운 속도로 발전하고 있고 내 코드는 언젠가 레거시가 됩니다. 새로운 것으로 갈아타야할 준비는 언제든지 해야하나 너무 빨리 갈아타도 탈이나고 너무 늦게 갈아타도 문제가 됩니다.

 

이러한 웹의 발전 역사와 방향성을 이해하면서 그 안에서 아직도 해결되지 않은 여러 불편함들에 대해서 숙지하고 그것들을 해소해주는 라이브러리나 패러다임에 대해서 빨리 깨닫고 공부해야 할 것을 찾고 새로운 기술로 갈아탈 수 있는 눈을 가질 수 있기를 저 역시 그리 되기를 희망합니다. 

 

이 글이 그 과정에 조금이나마 도움이 되었기를 바랍니다.

 

 

 

나의 생각

아키텍쳐의 의미와 앞으로 어떤 마음가짐을 가진채 라이브러리를 대하고 바라봐야 하는지,  
생각을 충분히 하지 못한채 라이브러리들을 사용하고자 했던건 아닌가 싶은 생각이 든다

 

 

출처 : https://velog.io/@teo/%ED%94%84%EB%A1%A0%ED%8A%B8%EC%97%94%EB%93%9C%EC%97%90%EC%84%9C-MV-%EC%95%84%ED%82%A4%ED%85%8D%EC%B3%90%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94

 

프론트엔드에서 MV* 아키텍쳐란 무엇인가요?

MVC, MVVM, MVI 아키텍쳐가 어쩌고 저쩌고... 소프트웨어를 공부하다 보면 한번쯤은 MV__로 시작되는 아키텍쳐라는 용어를 들어본적이 있을 겁니다. 실제로 프로그래밍을 할 때에는 중요하지 않아보

velog.io

 

728x90
반응형

+ Recent posts