본문으로 건너뛰기

· 약 18분
Mitchell

API 서버와의 통신 과정에서 발생할 수 있는 에러들과 이 에러들에 대한 적절한 처리를 통해 사용자 경험이 좋은 어플리케이션을 제공할 수 있습니다. 먼저 발생한 에러가 어떤 종류의 에러인지 파악하고 분류해야합니다. 에러가 파악되었다면 사용자가 처한 문맥에 맞게 적절한 에러처리를 진행하게 됩니다.

예제에서는 다음의 라이브러리들과 API에 대한 선수지식이 필요합니다.

  • Axios
  • TanStack Query
  • Error Boundary

에러 정의하기

에러가 어떻게 발생할 수 있는지를 중심으로 그 종류에 대해서 정리하겠습니다. 에러의 출처를 기준으로 에러의 종류를 나눠보겠습니다.

사용자 에러

에러의 원인이 애플리케이션 사용자에게 있는 에러입니다. HTTP Status Code 중 아래의 경우로 볼 수 있습니다.

  • 400: Bad Request - 잘못된 요청
  • 401: Unauthorized - 인증되지 않은 사용자
  • 403: Forbidden - 사용자 권한부족, 차단된 IP주소

클라이언트 에러

에러의 원인이 클라이언트 측에 있는 에러입니다. 개발단계에서 개발자의 실수로 발생할 확률이 높습니다. HTTP Status Code 중 아래의 경우로 볼 수 있습니다.

  • 404: Not Found - 존재하지 않는 URL 요청
  • 405: Method Not Allowed - 해당 URL로 허용되지 않은 메소드로 요청했을 때 (GET만 있는데 POST를 했다거나)
  • 그 외의 400번대 에러들

서버 에러

에러의 원인이 서버에게 있는 에러입니다. HTTP Status Code 중 아래의 경우로 볼 수 있습니다.

예상치 못한 에러

서버가 응답하지 않거나 네트워크 연결이 끊어진 경우 axios에서 다음의 에러를 전달합니다.

  • ECONNABORTED: 요청의 타임아웃
  • ENOTFOUND: 서버를 찾을 수 없는 경우

그 외 요청 과정에서 발생하는 예상할 수 없는 에러들이 있습니다.

· 약 5분
Mitchell

TanStack Query의 Mutation Cache와 meta 활용하기

Mutation을 중심으로 Mutation Cache와 meta 데이터를 활용하는 방법에 대해서 공유합니다. (Query와 관련된 활용에서도 동일합니다.)


QueryClient 살펴보기

TanStack Query를 사용하는 진입점에서 우리는 QueryClient 인스턴스를 생성하고 QueryClientProvider로 넘겨서 전역에서 해당 클라이언트를 사용할 수 있게 됩니다. 여기에서 QueryClient에 여러 설정을 해줄 수 있는데요. Mutation을 중심으로 살펴보면, defaultOptionsmutations, mutationCache 옵션이 있습니다.

· 약 5분
Mitchell

현재 앱의 컬러테마를 다루는 useThemeMode 커스텀 훅을 아래와 같이 테스트하려고 합니다. 기본 값은 'light'이며, changeTheme을 호출하여 테마를 변경할 수 있습니다.

const renderUseThemeMode = () => {
const { result } = renderHook(() => useThemeMode())

return result.current
}

describe('Test hook', () => {
test('render hook correctly', () => {
const { currentTheme, changeTheme } = renderUseThemeMode()

expect(currentTheme).toBe('light')

act(() => {
changeTheme('dark')
})

expect(currentTheme).toBe('dark')
})
})

첫번째 assertion에서는 예상대로 현재 테마가 기본 값인 'light'라고 통과하게 됩니다. 그러나 두번째 assertion에서 테마가 'dark'로 변경되지 않아 테스트가 최종적으로 실패하는 결과를 얻게 됩니다.

상태의 변화를 발생시키는 함수인 changeThemeact의 내부에서 실행하여 분명히 상태 변화가 발생하고, 테스트가 통과해야할텐데요. 왜 상태변화를 추적하지 못하고 있는걸까요?


문제의 원인

문제의 원인은 renderUseThemeMode 함수에 있습니다. 여러 테스트케이스에 걸쳐 편하게 훅을 렌더링하기 위해 만들어둔 함수인데요. 내부 구현을 보면 renderHook의 반환 값 중 result.current를 반환하는 것을 볼 수 있습니다. 여기가 바로 문제가 되는 부분인데요. 초기 상태에 대한 값이 result.current에는 들어있겠지만, 상태가 변경되더라도 이전의 값을 유지하기 때문에 변경된 상태로 assertion을 하면 실패하게 됩니다.


해결방법

해결방법은 아주 간단하지만 조금은 불편할 수 있는데요. result.current를 별도의 변수에 할당하지 않고 사용하면 상태 변화를 정상적으로 추적할 수 있게 됩니다. 이에 따라 renderUseThemeMode 렌더함수를 사용하지 않고, renderHook을 직접 호출하도록 변경하였습니다.

describe('Test hook', () => {
test('render hook correctly', () => {
const { result } = renderHook(() => useThemeMode())

expect(result.current.currentTheme).toBe('light')

act(() => {
result.current.changeTheme('dark')
})

expect(result.current.currentTheme).toBe('dark')
})
})

이제 정상적으로 상태 변화를 추적하여, 마지막 assertion을 통과할 수 있게 됩니다.


왜 그럴까?

그렇다면 왜 result.current을 다른 변수에 선언하면 상태 변화를 추적하지 못할까요? 우선 result.current의 정체가 무엇인지 renderHook 함수의 구현을 살펴보겠습니다.

function renderHook(renderCallback, options = {}) {
const {initialProps, ...renderOptions} = options
const result = React.createRef()

// 테스트용 함수 컴포넌트 생성
// 결과를 ref의 current로 전달
function TestComponent({renderCallbackProps}) {
// 전달한 커스텀 훅
const pendingResult = renderCallback(renderCallbackProps)

// 의존성 배열을 설정하지 않았으므로 매 렌더링마다 result.current의 값이 변경됩니다.
React.useEffect(() => {
result.current = pendingResult
})

return null
}

// 테스트 컴포넌트를 렌더, 즉 커스텀 훅을 실행합니다.
const {rerender: baseRerender, unmount} = render(
<TestComponent renderCallbackProps={initialProps} />,
renderOptions,
)

// 리렌러더 함수 생성
function rerender(rerenderCallbackProps) {
return baseRerender(
<TestComponent renderCallbackProps={rerenderCallbackProps} />,
)
}

return {result, rerender, unmount}
}

커스텀 훅의 변경된 상태가 React.creatRef로 생성된 result.current로 반영되도록 설계 되어있습니다. result.current의 참조는 React.creatRef로 생성되어 렌더링이나 상태변화에도 참조가 바뀌지 않습니다. 참조는 바뀌지 않는데, 그 안의 값은 바뀌기 때문에 renderHook의 result.current로 참조하는 경우 상태 변경에 대한 값을 지속해서 추적할 수 있게 됩니다.

· 약 22분
Mitchell

레거시 프로젝트의 코드베이스를 다른 코드베이스로 마이그레이션 하는 것은 서비스의 안정성 측면에서, 작업의 규모 측면에서 상당히 부담스러운 작업입니다. 프로젝트 전체를 한번에 마이그레이션 해야한다면 코드 마이그레이션 작업 자체로도 부하가 올 뿐 만 아니라, QA 측면에서도 세세한 부분까지 다시 검증해야하기 때문입니다.

사용자 입장에서는 어떨까요? 사실 마이그레이션은 사용자 입장에서는 어떠한 변화도 느끼지 못합니다. 내부적인 코드가 바뀔 뿐이니까요. 그런데 전체 마이그레이션을 진행해버리는 경우 내부적으로는 엄청 바쁜 과정을 통해 진행되지만 겉으로는 사용자에게 전혀 제공되는 이점이 없는셈입니다. 그러나 그런 선택을 했다는 것은 장기적인 측면에서 반드시 진행해야 하는 이유들이 있었기 때문입니다.

따라서 사용자에게 충분한 변화를 주면서 마이그레이션을 줄 수 있는 방법은 한번에 하는 것이 아니라 점진적으로 진행하는 것입니다. 레거시에서의 기능개발을 진행하면서도, 일부 페이지에 대해서는 마이그레이션 하는 방식으로 접근할 수 있다면 서비스의 성장과 품질을 모두 챙길 수 있을 것입니다.


Migration을 진행하는 이유

기존 제품은 21년도에 Vue2로 개발되어 현재까지도 서비스 중인 매장관리서비스입니다. 아래와 같은 이유들 때문에 Vue2에서 React로 마이그레이션을 결정하게 되었습니다.

Vue2의 지원 종료

2023년 12월 31일 기준 공식문서에 의하면 Vue2에 대한 지원은 공식적으로 종료되었습니다. 현재의 Vue는 Vue3을 공식으로 지원하고 있습니다. 따라서 Vue3나 React나 결국 마이그레이션이 필요한 상황에 처해있었습니다.

제품 복잡도

Vue는 데이터가 양방향으로 흐르도록 설계되어 있는데요. 제품 초기에는 빠른 개발속도와 직관적인 데이터 흐름으로 인해 장점으로 느껴졌었습니다. 그러나 제품이 성장함에 따라 다양한 데이터들이 컴포넌트에 따라 흐르게 되면서, 데이터 흐름을 추적하기가 점차 난해해지고 있는 문제가 있었는데요. 반면에 React에서는 부모에서 자식 컴포넌트로 데이터가 단방향으로 흐르기 때문에 여러 파일을 뒤져보지 않더라도 컴포넌트 구조를 따라 데이터 흐름을 파악하기 용이하였습니다. 이러한 측면으로 인해서 장기적으로 더 복잡해질 해당 제품의 코드 복잡도를 낮출 수 있겠다고 판단되었습니다.

생태계 및 커뮤니티 지원

Vue의 생태계에서도 충분히 지원되는 라이브러리나 도구들이 존재하지만, React가 가지고 있는 방대한 생태계에서는 확실히 부족해보입니다. Redux, Recoil, Jotai, Zustand 등 다양한 전역상태 관리 도구들이나, TanStack Query 같은 Data Fetching 라이브러리, 커뮤니티에서 찾아볼 수 있는 좋은 예제들과 프로젝트들이 React를 더욱 매력적으로 보이게 하는 요소들이였습니다.

그 외 사내의 상황

  • 프로젝트 구조가 Monorepo 환경으로 개편되면서, 공통 모듈을 원활하게 사용할 수 있게 되었습니다.
  • React로 개발된 디자인 시스템을 적용할 수 있어 브랜드에 맞는 디자인을 유지하면서도 개발 호율을 높일 수 있게 됩니다.
  • 타입스크립트로의 마이그레이션하여 정적 타입 분석을 통해 안정성을 더욱 높일 필요가 있었습니다.
  • 프론트엔드의 코드베이스 단일화가 필요합니다.

점진적으로 마이그레이션 아이디어 (Module Federation)

말머리에서 왜 마이그레이션을 점진적으로 진행해야 하는지는 설명하였습니다. 그런데 어떻게 하면 Vue로 작성된 코드들을 React로 점진적으로 마이그레이션 할 수 있을까요? 해당 문제에 대한 해결책은, 페이지 별로 구분하여 점진적으로 React로 바꾸어나가는 것인데요. 결론적으로 말하자면, Module Federation을 활용하여 Vue 프로젝트 안에서, 일부 페이지들을 런타임에 통합하여 제공하는 방식으로 접근하면 가능하게 됩니다.

Module Federation

Module Federation은 마이크로프론트엔드 아키텍쳐로 서비스를 제공할 수 있는 핵심 아이디어인데요. 하나의 코드베이스에서 프로젝트를 배포하는 것이 아니라, Feature를 기준으로 서비스를 독립적으로 배포하고, 느슨하게 연결해줄수 있는 구조입니다. 즉 하나의 서비스를 여러 마이크로 서비스로 나누고, 하나의 웹사이트에서 하나의 서비스 처럼 동작할 수 있도록 해줍니다.

핵심 컨셉

  1. 기술 독립성: 각 단위는 기술적으로 독립적이여야 한다.
  2. 컨텍스트 독립성: 각 단위는 애플리케이션을 자제척으로 구축해야하며, 다른 곳의 상태를 공유하거나 변수에 의존하지 않도록 한다.
  3. 네임스페이스를 활용한 분리: 각 작동의 단위 격리가 불가능한 경우, 네이밍 컨벤션에 따라 prefix 등으로 네임스페이스를 활용한다.
  4. 기본 브라우저 기능 활용: 단위간의 통신을 위해 자체 시스템을 구축하기 보다는 브라우저 이벤트, 커스텀이벤트를 활용하고 필요한 경우 최대한 간단하게 유지한다.
  5. 탄력적인 웹 디자인 구축: 자바스크립트가 에러가 있거나 실행할 수 없어도, 기능은 사용가능해야 하며, 범용렌더링과 점진적 향상을 통해 성능을 향상시킬 수 있다.

도입시 장점

  • 확장성
  • 독립적 개발 및 배포
  • 더 빠른 배포
  • 팀 자율성 강화
  • 업데이트 및 유지관리 용이

도입시 단점

  • 통합의 복잡성
  • 전체 앱 번들 크기가 증가, 로드 시간 증가로 성능문제
  • 배포, 모니터링, 확장을 위한 오버헤드가 발생하고, 네트워크 비용증가
  • 일관된 코딩 표준을 유지하고 팀 간의 의존성 관리에 강력한 거버넌스가 필요하며 이에 따라 의사결정이 느려질 수 있음.

즉 요약하면, 제품 자체의 크기가 매우 크고 무거우며, 다양한 팀이 속해 있을 때 마이크로프론트엔드 아키텍쳐를 통해서 다양한 문제를 해결할 수 있다는 것인데요. 사실 저희 제품과 회사가 처한 상황과는 거리가 멉니다. (하나의 팀, 그렇게 크지 않은 서비스)

그럼에도, 하나의 코드베이스로 국한될 수 있는 상황에서 일부 페이지들을 독립적으로 분리하여 개발하고 런타임에서 통합시킬 수 있다는 특징과 하나의 Host App과 하나의 Remote App만 가지고 있으므로, 통합에서의 복잡도도 크지 않기 때문에 React로의 마이그레션 과정에서 임시로 사용할만한 해결책으로 채택하게 되었습니다.


Vue2에서 React 렌더링하기

React로 마이그레이션 할 것이기 때문에 당연히 마이그레이션 대상이 되는 페이지들을 React로 구현해야 합니다. 페이지 구현이 완료되었다면, 이제 런타임에서 해당 페이지를 보여주어야 합니다.

Module Federation 설정하기

Webpack이나 Vite의 플러그인으로 제공되는 Module Federation을 이용해 설정할 수 있습니다. 저희 예제에서는 Vite를 사용하였습니다.

Vue와 React에서 아래의 플러그인 설치하세요.

npm install -D @originjs/vite-plugin-federation

React

vite.config.js
export default defineConfig({
plugins: [react(), federation({
name: "remote-app",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/App",
},
shared: {
react: {
requiredVersion: "^18.2.0",
},
"react-dom": {
requiredVersion: "^18.2.0",
},
},
})],
build: {
target: 'esnext'
}
})
  • 가져오는 대상이 되는 앱을 Remote라고 부릅니다.
  • DOM 요소에 바인딩하는 작업은 Vue 안에서 진행할 예정입니다. 따라서 React에서는 진입점인 App 파일만 내보내도록 설장합니다.
  • shared 설정을 통해서, 같은 버전의 리액트를 여러번 로드하지 않도록 해줍니다.

Vue

vite.config.js
export default defineConfig({
// ...

plugins: [
vue(),
federation({
name: 'host-app',
remotes: {
remoted: 'http://localhost:4173/assets/remoteEntry.js',
},
shared: ['react', 'react-dom'],
}),
],
build: {
target: 'esnext'
}

// ...
})
  • 리모트를 가져오는 주체가 되는 앱을 Host라고 합니다.
  • remotes에서 가져올 앱의 리모트를 등록합니다. 여기에서는 remoted라는 키로 맵핑하였습니다.
  • shared 설정을 통해서, Remote App에서 패키징 시간을 절약, React, react-dom이 여러번 다운로드 되는 것을 방지합니다.

Vue2 안에서 React 렌더링하기

프레임워크를 떠나서 기본적으로 모두 자바스크립트라는 사실을 잊지 말아야합니다. 즉, Vue 안에서 React를 분명히 렌더링할 수 있다는 말입니다. CRA나 vite-create로 시작할 때 index.js에서 리액트를 DOM에 Mount 했던 코드를 Vue 안에서 실행시켜주면 됩니다.

<template>
<div>
<!-- Vue의 페이지가 렌더링 됩니다. -->
<router-view>
<!-- React의 페이지가 렌더링 됩니다. -->
<div ref="reactRoot"></div>
</div>
</template>

<script>
import React from 'react'
import ReactDOM from 'react-dom/client'

export default {
methods: {
//...
async renderReactApp() {
// 리모트 앱에서 App를 불러온다.
const reactApp = await import("remoted/App");

// id가 reactRoot인 DOM 요소에 reactApp을 렌더한다.
ReactDOM.createRoot(this.$refs.reactRoot).render(
React.createElement(reactApp.default)
);
},
//...
},
};
</script>

React 페이지 라우팅하기

단편적으로 생각했을 때 리액트로 헨더할 페이지 마다 Vue 파일을 생성하고, vue-router에서 해당 되는 url에 연결시켜주면 되겠다고 생각할 수 있습니다. 그러나 불필요하게 React를 페이지마다 다시 마운트해야 되고, 코드가 중복을 발생되는 문제가 있을 뿐만 아니라. 리액트 페이지들끼리 간에도 새로 렌더링하기 때문에 상태를 공유할 수 없다는 치명적인 문제가 발생할 수 있습니다.

따라서 reactApp은 딱 한번 초기화되도록 해야합니다. 저희는 Vue의 진입점이 되는 App.vue 파일 안에서 딱 한번만 React의 리모트를 불러와 렌더링 할 것입니다. 그리고 URL 구분을 통해 Vue app이 렌더링할지, React app이 렌더링할지 결정하도록 설정하였습니다.

Vue Router
const router = new Router({
mode: 'history',
routes: [
// ...
{
path: '/react/*',
component: null
},
// ...
]
})
  • Vue 페이지 라우팅을 관리하는 Vue Router의 일부입니다. path에 '/react'가 포함되어 있다면, Vue에서는 어떤 것도 렌더링 하지 않도록 component에 null을 할당합니다.
React App
function App() {
switch (location.pathname) {
case '/react/some-page':

return <SomePage />

// ...

default:
return null
}
};

export default App
  • React에서는 현재 url의 pathname에 따라 적절한 페이지가 렌더링 될 수 있도로 설정합니다. Vue에서는 '/react'가 포함된 페이지에서 아무것도 렌더링하지 않지만, React에서는 해당 url에 대해 적절한 페이지들을 렌더링 할 것입니다.

그러나 여기에서 문제가 있습니다. location.pathname은 리액트가 변경을 추적하는 상태가 아닙니다. 따라서 Vue 쪽에서 페이지를 변경한다고 하더라도, React에서는 페이지 변화를 알 수가 없습니다. 따라서 Vue의 변경이 React에 미치도록 해야합니다.


커스텀 이벤트를 통한 페이지 변화 감지

Vue에서는 Vue Router를 통해서 페이지 변화를 정상적으로 감지 할 수 있습니다. 이를 React에 알려주기 위한 방법으로 페이지 변화에 대한 커스텀 이벤트를 만들어 Vue에서 이벤트를 발생시키고, React에서는 이 커스텀 이벤트에 대한 이벤트리스너를 등록하여 페이지 변화에 따른 변화가 발생하도록 해줄 것입니다.

Vue Custom Event
router.beforeEach(async (to, from, next) => {
if (to.path.startsWith('/react')) {
const routeChangeEvent = new CustomEvent('routeChange', {
detail: { to: to.path }
})

window.dispatchEvent(routeChangeEvent);
}
})
  • Vue Router의 라우트 가드를 통해서, 페이지 진입전 경로를 체크하여, react로의 페이지 이동이라면, routeChange 라는 커스텀이벤트를 생성하고 이벤트를 발생시킵니다.
React Listen Custom Event
function App() {
const [path, setPath] = useState(location.pathname)

useEffect(() => {
const routeChangeHandler = (event: { detail: { to: string; }; }) => {
const { to } = event.detail;
setPath(to)
};

window.addEventListener('routeChange', routeChangeHandler);

return () => {
window.removeEventListener('routeChange', routeChangeHandler);
};
}, []);

switch (path) {
case '/react/some-page':
return <SomePage />;

// ...

default:
return null;
}
};

export default App;
  • React에서는 routeChange 이벤트에 반응하여 현재 pathname을 업데이트 하는 콜백함수를 이벤트 리스너로 추가합니다.
  • 이제 Page 변화에 따라 정상적으로 알맞는 React 페이지들이 렌더링 할 수 있게 됩니다.

분리된 앱들과 상태 공유하기

사실 분리된 App에 공유상태를 두는것은 좋지 않습니다. 공유 상태를 앱들이 구독하게 되는 경우, 앱들간의 종속성 생겨 앱의 독립성을 보장할 수 없기 때문입니다. 이 때문에 메모리의 상태를 공유하기 보다는 스토리지를 이용하여 간접적으로 접근하는 방법을 차용하는 게 좋습니다. (로컬스토리지, 세션스토리지, 쿠키 등)

그럼에도 불구하고 필요한 경우가 생기기 마련인데요. 저희의 경우에는 JWT로 만들어진 Access Token을 스토리지에 저장하지 않고, 메모리에 저장하여 휘발적으로 관리하고 있었습니다. 따라서 발급받은 AccessToken을 마이크로 앱에서 모두 접근할 수 있어야 했습니다.

Access Token 저장하고 공유하기

마이크로 앱 간에 토큰에 접근하기 위해서는 결국 전역을 통해서 접근하는 방법 밖에는 보이지 않는데요. 전역에 대놓고 액세스 토큰을 선언하고 할당해버리는 경우 누구누 쉽게 접근할 수 있기 때문에 위변조가 쉽게 발생할 수 있어 위험합니다. 따라서 closure를 활용하여 액세스토큰 변수를 은닉하고, 함수를 통해서 저장하고 꺼내올 수 있도록 처리하였습니다.

function tokenHub() {
let _accessToken = null;

return {
getAccessToken() {
return _accessToken
},
setAccessToken(accessToken) {
_accessToken = accessToken
}
}
}

if (window) {
window.federationShared = {
...tokenHub()
}
}
  • tokenHub를 호출하여 반환되는 함수를 window 전역객체에 등록합니다.
window.federationShared.getAccessToken // 액세스토큰 값
  • Host이든 Remote이든 window.federationShared 통해서 전역변수에 접근이 가능합니다.

마치며

이 글에서의 주요 목적은 Vue에서 React로 점진적 마이그레이션을 적용하는 과정에서 Module Federation을 활용하는 방법과 그 상황에서 발생할 수 있는 여러 문제를 해결하는 방법을 공유하였습니다. 마이그레이션을 계속 진행할수록 새롭고 다양한 문제들을 마주할 것으로 예상되는데요. 그럼에도 점진적으로 제품을 마이그레이션 할 수 있는 방법이 있어 다행입니다 😂

· 약 6분
Mitchell
npm run tsc

타입스크립트를 자바스크립트로 컴파일해주는 CLI(Command Line Interface)입니다. 간단한 명령어 하나만 입력하면 되어 굉장히 편리한데요. 최근 개인 프로젝트로 개발 생산성을 높여주는 라이브러리 하나를 만들고 있는 중, 모듈을 실행하기 위해 직접 node dist/main.js 처럼 경로를 직접 타이핑하는것이 사용자 입장에서 불편하다 판단되었습니다. 그래서 타입스크립트의 tsc 처럼 간단한 CLI를 만들어 실행의 편리함을 높이려 합니다.

실행원리 톺아보기

먼저 타입스크립트의 tsc가 어떻게 실행되는지를 쫓아가며 이해해보겠습니다. 혹시 타입스크립트가 설치되어 있지 않다면 아래 명령어를 실행한 후 따라와주세요.

npm i typescript -D

npm run

먼저 npm run <명령어>라는 명령어는 package.jsonscript 영역에 정의된 dev, start, build 와 같은 실행 스크립트들을 실행해주는 역할을 합니다. 그런데 스크립트 상에서는 tsc로 정의된 스크립트가 보이지 않습니다. 그렇다면 npm run은 무엇을 실행하게 되는걸까요?

bin

패키지들이 설치되어 있는 node_modules 디렉토리를 살펴보면 가장 위에 숨김 디렉토리인 .bin 디렉토리가 존재합니다. .bin 디렉토리 안을 살펴보면 tsc라는 파일을 확인할 수 있는데요. 아마 npm run tsc 이 파일을 실행할 것으로 추측해볼 수 있습니다.

여기에서 bin은 binary의 준말로 실행가능한 파일들이 존재하는 디렉토리입니다. 운영체제 단에서도 마찬가지로 bin 디렉토리 안에 실행가능한 바이너리 파일들이 모여있으며, PATH 환경 변수가 bin 디렉토리를 참조해 실행하는데 이러한 동작방식과 동일합니다.

tsc

.bin 디렉토리 안에 존재하는 tsc를 열어보면 다음과 같이 간단한 코드가 들어있습니다.

#!/usr/bin/env node
require('../lib/tsc.js')

.bin 디렉토리 안의 tsc 파일은 실행 스크립트 형태로 되어 있어 node 명령어를 쓰지 않아도 npm run으로 실행이 가능합니다. 그러나 실제로는 해당 스크립트가 node로 실행이 되어야 하기 때문에 첫 줄의 shebang(#!로 시작하는 첫줄)에 node로 해당 스크립트 파일을 실행하도록 표시하게 됩니다.

정확히 말하면 node_modules.bin 안에 있는 이 파일은 심볼릭 링크로 실제 모듈이 들어있는 디렉토리의 bin 파일의 실행파일을 가리키고 있으며, npm run tsc는 실제 모듈의 스크립트를 실행합니다.

내 패키지의 명령어 만들기

원리도 크게 복잡하지 않기 때문에 역시 만드는 방법도 크게 어렵지 않습니다.

01. 실행파일 만들기

루트경로에 bin 디렉토리를 하나 만들고 그 안에 cli.js를 생성합니다.(파일경로가 꼭 bin에 있어야하는 것은 아닙니다!) 여기에서는 실행할 파일이 루트경로에 있는 main.js라고 가정하겠습니다. cli.js에 다음과 같은 실행코드를 작성합니다.

#!/usr/bin/env node
require('../main.js')

02. 명령어를 등록하기

이 예시에서는 example-nice라는 명령어로 모듈을 실행하려고 합니다.

package.json의 bin 설정에 명령어와 1번의 실행파일 경로를 등록합니다.

"bin": {
"example-nice": "./bin/cli.js"
}

03. 빌드 및 배포하고 사용하기

이제 해당 프로젝트를 빌드하고 배포하고나면, npm install로 설치시 node_modules.bin 디렉토리안에 example-nice라는 심볼릭 링크가 생성되어 있는 것을 확인할 수 있습니다.

이제 아래와 같이 나만의 명령어를 통해 원하는 파일을 실행할 수 있게 됩니다.

npm run example-nice

· 약 6분
Mitchell

문제의 상황

const fruits = {
apple: 'apple',
banana: 'banana',
orange: 'orange'
}

const keys = Object.keys(fruits)

keys의 타입은 어떻게 추론될까요? 아마 ('apple' | 'banana' | 'orange')[]가 될거라고 예상할 수 있습니다.

그리고 이 keys를 이용해서 같은 키를 가지면서 값에 ' gift'라는 문자열을 추가하는 fruitGift를 만들어보려고 합니다.

const fruits = {
apple: 'apple',
banana: 'banana',
orange: 'orange'
}

const keys = Object.keys(fruits)

const fruitGift = keys.reduce((result, key) => {
result[key] = fruits[key] + 'gift'
return result
}, {} as typeof fruits)

그러나 keys의 배열의 reduce 함수 내부에서 제공되는 단일 key 값으로는 아래의 에러와 함께 fruits에 접근할 수 없습니다.

Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{ apple: string; banana: string; orange: string; }'. No index signature with a parameter of type 'string' was found on type '{ apple: string; banana: string; orange: string; }'.

왜냐하면 Object.keys로 얻은 값의 타입추론이 ('apple' | 'banana' | 'orange')[]로 특정된 것이 아니라 string[]로 되기 때문입니다.

· 약 6분
Mitchell

문제의 발견

모달을 제작시, 모달을 아래쪽으로 드래그하여 꺼질 수 있는 기능을 framer-motion을 사용하여 구현하였습니다. 애니메이션은 모두 정상적으로 잘 작동하였는데요, 문제를 하나 만나게 됩니다.

모달 안에 위아래로 드래그하여 시간을 선택하는 TimePicker를 배치해야했습니다. 그런데 TimePicker에서 시간을 선택하려고 드래그 하는 순간 모달 전체가 움직여버립니다!

따라서 TimePicker 영역에서 드래그를 시도할 경우, 그 이벤트가 모달 전체로 전파되는 것을 막아 각 영역에서 별도의 드래그가 가능하도록 처리하려는 아이디어로 접근하였습니다. 결론적으로 이벤트 전파를 막는 방식으로는 이 문제를 해결 수 없습니다.

왜냐하면 React가 관리하는 DOM과 framer-motion이 관리하는 DOM이 다르기 때문에 우리가 일반적으로 하는 event.stopPropagation()으로 이벤트 전파를 막을 수 없기 때문입니다.


· 약 4분
Mitchell

마주한 상황

서버로부터 특정 점검과 관련된 리스트를 받아 그 데이터의 URL로 접근하여 정보를 다시 가져와야하는 경우가 발생하였습니다.

예를 들어 다음과 같은 데이터를 서버로부터 전달 받을 것입니다.

const results = [
{
id: 1,
dataUrl: 'https://wwww.abcd.efg/abc.json'
},
...
]

진짜로 필요한 정보는 results 배열에 담겨있는 객체가 아니라, 그 객체의 dataUrl로 다시 get 요청을 통해 가져올 정보들입니다. 즉, 배열을 순회하면서 정보들을 가져와야합니다.

Promise.all

여러 비동기처리를 한번에 처리해야할 때 쓰이는 Promise.all로 배열에 포함된 데이터들을 가져오려고 시도했습니다.

function fetchData(url) {
const response = await fetch(url);
const jsonData = await response.json()
return jsonData
}

const promiseResults = Promise.all(results.map(async result => {
const data = await fetchData(result.dataUrl)

return {
...result,
...data
}
}))

생각해보니, 모든 데이터의 dataUrl에 정확한 주소가 없을 수도 있습니다. 이에 따라 Promise.all 안의 이행중 dataUrl이 없거나 올바르지 않은 주소가 있을 때, 정상적으로 가져와야하 하는 데이터 마저도 얻지 못하게 됩니다.

왜냐하면, Promise.all은 하나라도 reject되면 전체가 reject되기 때문입니다.

Promise.allSettled

정확히 원하는 동작은, dataUrl을 가지고 있다면 해당 URL로부터 추가적인 데이터를 가져오고 그렇지 않다면 기존의 데이터를 유지하는 것이였습니다.

따라서 배열의 비동기 동작 중 reject의 발생과 상관없이 모든 작업을 수행하는 Promise.allSettled 메소드를 이용해야 합니다.

function fetchData(url) {
const response = await fetch(url);
const jsonData = await response.json()
return jsonData
}

const promiseResults = await Promise.allSettled(results.map(async result => {
const data = await fetchData(XPathResult.dataUrl)

return {
...result,
...data
}
}))

여기서 유의해야할 점은, Promise.allSettled의 반환값이 results.map의 반환 모양대로 나오지 않는다는 것입니다. 예상되는 반환 값은 다음과 같으며, results.map의 반환으로 나와야하는 모양은 values 안에 담겨있게 됩니다.

[
{
status: 'fulfilled',
values: {
...
}
},
{
status: 'rejected',
reason: 'failed'
}
]

따라서 해당 동작에 유의하여 다음과 같이 다시 변환해야 합니다.

function fetchData(url) {
const response = await fetch(url);
const jsonData = await response.json()
return jsonData
}

const promiseResults = await Promise.allSettled(
results.map(async result => {
const data = await fetchData(result.dataUrl)

return {
...result,
...data
}
})).then(promisedResults => {
return promisedResults.map((result, idx) => {
// 기존 서버데이터
...results[idx],
// dataUrl로 불러온 데이터
...result
})
})

then의 최종 변환 과정은 최소 서버데이터를 유지하면서 dataUrl로부터의 데이터가 추가되도록 변환하는 과정입니다.

· 약 34분
Mitchell

현대의 웹을 아름답게 만들어 내는데 있어 CSS는 필수적인 요소입니다. 그러나 Vanilla CSS(순수 CSS 자체)가 가지고 있는 명확한 한계점이 존재합니다. 이에 따라 CSS를 효과적으로 사용하기 위한 도구들이 생겨났는데요. CSS 문법을 확장한 Sass와 같은 preprocessor, CSS도 JavaScript로 관리하겠다는 CSS in JS, 그러한 CSS in JS의 단점을 극복하면서 떠오르는 Zero-runtime CSS 등 이처럼 CSS를 효과적으로 작성하기 위한 선택지에는 여러가지가 있습니다.

특히 CSS in JS는 Trade-off가 확실한 CSS 작성 방법입니다. JavaScript로 함께 작성하면서 DX(Developer Experiece)에 이점을 취할 순 있지만, 런타임에서 스타일이 생성되어야 한다는 태생적인 한계 때문에 성능은 다른 선택지에 비해 확실히 좋지 않습니다.

그럼에도 CSS in JS를 사용하기로 결정했다면, 그 장점은 살리되 단점은 최소화하는 방법으로 접근해야합니다. 따라서 이 글은 최종적으로 어떻게 하면 더 효과적으로 사용할 수 있을지 Emotion(CSS in JS 라이브러리 중 하나) 사용을 중심으로 6가지 방법을 제안합니다.

긴 글을 읽을 시간이 없다면 효과적인 Emotion(CSS in JS) 사용을 위한 6가지 방법으로 바로 이동하세요.

· 약 8분
Mitchell

비전공, 독학개발로 시작하여 2년까지는 프론트엔드 개발자로서 리눅스 커맨드라인에 대한 지식 하나 없이도 큰 문제가 없이 주니어개발자 생활을 해왔다고 생각했었습니다. 어언 3년차로 접어들고 있는 지금 devServer, 빌드, CI/CD 등 상세히 조작하고 알아가야하는 영역이 넓어지면서, 셸, 커맨드라인에 대한 이해가 필요해지고 있다고 느끼고 있었습니다.

때마침 운좋게도 책 리뷰어로 선정되어 리눅스 커맨드라인에 대한 책 리뷰를 하게 되는 기회를 얻어 이 글을 작성하게 되었습니다.


책 이미지

  • 제목: Efficient Linux at the Command Line 효율적인 리눅스 명령어 사용의 기술
  • 저자: 대니얼 J. 바렛
  • 옮긴이: 심효섭
  • 출판사: 길벗