반응형

이 페이지는 사실 서버사이드 렌더링을 사용하진 않는다.

우선 SSR 의 전제조건은 다음과 같다.

1. CRA 를 Eject 하지 않는다.
한번 eject 하면 되돌릴 수 없고, react-scripts 가 해주는 많은 편의기능을 포기하는 동시에 업데이트도 골치아파 진다. timarney/react-app-rewired 와 같은 패키지도 존재하지만 이 역시 메이저 업데이트를 바로 지원하지 않을 가능성이 높고, create-react-app 의 dependency 도 충분히 피곤하다.

2. 전체 페이지를 렌더링한다
SEO 를 위해서 전체 페이지 렌더링이 필수적인 것은 아니나 분명 전체 페이지를 그리는 것이 좋은 것은 자명하고, First Meaningful Paint 라는 측면에서도 그렇고, 유저에게 매번 로딩화면을 보여주고 싶지 않았다.

3. 로그인 처리도 한다.
많은 SPA 가 로그인처리를 browser 에 jwt 등을 저장하는 방식을 사용하여, 로딩 시에 다시한번 서버쪽 API 를 접근하게 하는데, 이 부분을 역시 해소하고 싶었다. 가급적이면 서버에서 처리를 해서 한번 화면이 그려지면 repaint 되는 부분을 최소화 하고 싶었다.

 

4. TTFB 를 고려한다.
사실 초반에는 큰 문제가 없었고, 어플리케이션에 담기는 데이터 양이 늘어나면서 생겨난 문제로 어쩔 수 없는 부분이 있더라도 속도를 개선하고 싶었다.

1. CRA 를 eject 하지 않는다.

QWER.GG 는 CRA 의 CSS 를 SASS 로만 변경하여 사용하고 있다. 따라서 일반적인 component 의 구조는 다음과 같다.

import ‘./Button.scss’;

import React from 'react';
import classNames from 'classnames';

type Props = {
  className?: string;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

const Button: React.FC<Props> = ({ className, ...props }) => {
  return (
    <button 
      className={classNames('Button', className)}
      {...props}
    />
  );
}

export default Button;

때문에 두가지 제약사항(?)이 생겨난다.

  1. Typescript 를 컴파일 해야 한다.
  2. Sass 파일이 ts 코드 상에서 import 된다.

서버에서 react 코드에 접근하려면 build 되어야 하고, CRA 의 build process 는 eject 하지 않으면 접근할 수 없기 때문에 다른 방법을 사용해야 했다. 다행히 typescript 의 기본 compiler 인 tsc 가 jsx 또한 문제없이 처리하기 때문에, tsc 를 사용하기로 했다. 그런데 import ‘./Button.scss’; 이 부분이 문제였다.

첫번째 시도 - 빌드시에 scss 파일을 스킵하자

.ts  .js 같은 일반적인 스크립트 extension 이 아니면 tsc 는 처리할 수 없고, 빌드만 스킵하더라고 해도 해당 코드가 script 상에 포함되어 있어, node.js 런타임 에서 해당 component 를 불러와 서버 사이드 렌더링을 시도할 때 syntax error 를 throw 하게 된다. 때문에 다른 방법이 필요했다.

두번째 시도 - 전처리

CRA 에서 webpack 을  이미 포함하기 때문에 다시 설치해서 빌드할 수 없었고, webpack 빌드는 기본적으로 browser 를 위한 것이라 node.js 서버 쪽 빌드와 잘 어울리지 않았다. 애초에 node.js server 에서는 code 를 하나의 .js 파일로 만들 이유가 없기 때문에 불필요한 요소라 생각했다. 물론 그렇게 해도 되었겠지만, 경험상 어울리지 않는 조합을 엮는 행위는 미래의 생산성에 분명히 영향을 끼쳤다.

webpack 을 쓰지 않고 전처리를 하려면 무엇을 사용할까 고민했다. 태초에 gulp 가 있었다. webpack 이전에 많이 사용되던 툴 중 grunt 는 설정파일이 귀찮으니 패스., gulp 를 사용하기로 했다. 코드는 상당히 단순하다 .scss 구문이 있는 줄을 찾아내서 // Removed 로 교체한다. 코드는 다음과 같다.

 
const gulp = require(‘gulp’);
const replace = require(‘gulp-replace’);

gulp.task(‘build’, () => {
  return gulp.src(‘src/**/*.{ts,tsx}’, { base: ‘src’ })
    .pipe(replace(/.+\.s?css.+/g, ‘// Removed’))
    .pipe(replace(/.+\.yml.+/g, ‘// Removed’))
    .pipe(gulp.dest(‘server’));
});

세번째 시도 - 빌드 스크립트로 통합하기

일단 여기까지 했으니 큰 난관은 헤처왔고, 나머지는 빌드 과정에 녹여내면 된다.

{
  "scripts:" {
    “build:server”: “rimraf server && gulp build && npm run tsc”,
    “tsc”: “node node_modules/typescript/bin/tsc -p tsconfig.server.json”,
  }
}

tsconfig.server.json 이란 파일을 새로 만든 이유는 약간의 설정이 달라야 하기 때문이다. CRA 의 webpack 은 기본적으로 tsc 는 typecheck 만 하고 실제 jsx 나 code compile 은 babel 을 사용하기 때문에 두 타겟의 설정은 달라져야 한다. 자세한 설명보다는 현재 사용하고 있는 설정 파일을 첨부한다.

아래 파일이 일반적으로 react 에 필요한 tsconfg.json 이고

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "es6",
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "typeRoots": [
      "@types"
    ],
    "jsx": "preserve"
  },
  "include": [
    "src"
  ]
}

필요없는 부분을 삭제하고 일부 수정한 tsconfig.server.json 이다.

{
  "compilerOptions": {
    "outDir": "build/",
    "module": "commonjs",
    "target": "es6",
    "noImplicitAny": false,
    "sourceMap": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "jsx": "react",
    "moduleResolution": "node"
  },
  "include": [
    "server"
  ]
}

오래전에 작성한 코드라 불필요한 설정이 들어가있을 수 있으나 그냥 첨부한다. 이 정도라도 누군가에겐 큰 도움이 될 수 있으니.

2. 전체 페이지를 렌더링한다.

초기부터 코드가 많이 변경되어 몇가지 핵심적인 요소만 언급한다. QWER.GG 는 Restful API 를 redux store 에 저장하는 방식으로 개발을 시작해서 현재 Apollo 기반 GraphQL 을 사용하고 있다. 아직도 redux state 를 완전히 걷어내지는 못했고 따라서 redux state 주입 코드와 apollo 데이터 주입 코드가 공존하고 있다.

1. 어떻게 state 를 주입할까?

기본적으로 redux 는 서버 렌더링이 간단하다. react-redux  Provider 에 store 를 만들어서 주입해주면 된다. 그런데 문제는 CRA 를 그대로 사용하려면 템플릿을 사용하지 않고 index.html 을 그대로 살려야 한다는 점이다. eject 하면 간단하겠지만, 상술했듯 최대한 원형을 보존하고 react-scripts 의 업데이트 이점을 살리는 것이 기본 전제였다.

일단 파일을 불러온다.

const indexTemplate = fs.readFileSync(path.resolve(__dirname, ‘../../build/index.html’), ‘utf8’);

여기서 주의할 점은 빌드된 index.html 파일을 불러오는 것이다. 이 부분도 참 귀찮은 부분 중 하나였는데 빌드 되기 전 index.html 을 사용하게 되면 빌드된 <script src="..." />  구문이 포함되어 있지 않기 때문에 반드시 cra 의 build 된 이후의 코드를 사용해야 한다.

 

그 다음 필요한 코드를 다음과 같이 String.prototype.replace  를 사용해서 교체해준다. cheeriojs/cheerio 를 쓰면 훨씬 쉬웠겠지만 그땐 거기까지 생각이 닿지 않았고, 다음과 같이 다소 원시적인 방법으로 수정해주었다.

export function renderDefault(req: express.Request) {
  const language = getLanguageFromRequest(req);
  const theme = getThemeFromRequest(req);
  const helmet = Helmet.renderStatic();

  return indexTemplate
    .replace('lang="en"', `lang="${language}"`)
    .replace('data-theme="dark"', `data-theme="${theme}"`)
    .replace('window.__language__', `window.__language__='${language}'`)
    .replace(/<title>.*?<\/title>/g, helmet.title.toString())
    .replace('<meta name="placeholder">', helmet.meta.toString())
    .replace('<link rel="placeholder">', helmet.link.toString());
}
 

킬링 포인트는 index.html 에 넣어준 placeholder 코드들. 다시 말하지만 cheerio 를 사용하면 훨씬 덜 원시적으로 처리할 수 있다.

<html lang="en" data-theme="dark">
  <head>
    <meta name="placeholder">
    <link rel="placeholder">
    ...

2. 실제로 데이터 주입하기

실제로 데이터를 주입할때는 그냥 절차적으로 진행했다. default state 를 가져와서 부어주는 방식. 역시 원시적인 String.prototype.replace 로 교체해주었다. 다른점이라면 index.html 이 아니라 renderToString 된 코드를 수정했다는 정도.

export async function renderFromServer(req: express.Request) {
  const preloaded = await getDefaultState(req);
  const apolloClient = getApolloClient(req);
  const hydratedApp = await getHydratedApp(req, preloaded, apolloClient);

  // TODO: 여기가 가장 느림. 퍼포먼스 개선 포인트
  await getDataFromTree(hydratedApp);

  const rendered = ReactDOMServer.renderToString(hydratedApp);

  return renderDefault(req)
    .replace(‘window.__PRELOADED_STATE__={}’, `window.__PRELOADED_STATE__=${JSON.stringify(preloaded).replace(/</g, '\\u003c')}`)
    .replace(‘window.__APOLLO_STATE__={}’, `window.__APOLLO_STATE__=${JSON.stringify(apolloClient.extract()).replace(/</g, '\\u003c')}`)
    .replace(‘<div id=“root”></div>’, `<div id="root">${rendered}</div>`);
}
 

마지막으로 말하는데 왠만하면 cheerio 를 쓰자. 결과물은 devtools 로 보면 이렇게 된다. window.__APOLLO_STATE__ 의 일부분인데 이런걸 browser 에 뿌려줘야 한다는 사실이 진짜 너무 괴롭다.

브라우저에서 반복 쿼리를 하지 않기위한 preloaded state

async function getHydratedApp(
  req: express.Request,
  preloadedState: any,
  apolloClient: ApolloClient<NormalizedCacheObject>,
) {
  const url = req.originalUrl;
  const { store } = configureStore(preloadedState);
  const language = getLanguageFromRequest(req);

  return (
    <ApolloProvider client={apolloClient}>
      <Provider store={store}>
        <StaticRouter location={url} context={{ url }}>
          <GlobalStateProvider cookies={req.cookies}>
            <App language={language} />
          </GlobalStateProvider>
        </StaticRouter>
      </Provider>
    </ApolloProvider>
  );
}

Provider 가 무척 많다고 생각되는 건 기분 탓이다. hook, redux, apollo, react-router 가 섞여 있다보니 자연스럽게 이렇게 된다. 왜 이렇게 됬냐고 하기 전에 싱페어 (SPA) 의 피로감 이 글을 읽고 오자. 다만 이렇게 하다보니 위에 주석에도 남겼지만 Performance 문제가 발생한다. 후술하겠다.

3. 로그인 처리도 한다.

로그인 처리를 위해서는 Cookie 에서 로그인 처리를 해주어야 한다. browser 가 가지고 있는 long time storage 중 서버와 동시에 사용할 수 있는 유일한 부분이기 때문이다. 요즘엔 fetch 도 표준이 바뀌어서 same-origin 기준 cookie 를 기본적으로 보내기 때문에 다소 편리해지긴 했다. Using Fetch - Web API | MDN 링크를 참고하자.

* 보통 fetch는 **쿠키를 보내거나 받지 않습니다.**  사이트에서 사용자 세션을 유지 관리해야하는 경우 인증되지 않는 요청이 발생합니다. 쿠키를 전송하기 위해서는 자격증명(credentials) 옵션을 반드시 설정해야 합니다.
 [2017년 8월 25일](https://github.com/whatwg/fetch/pull/585)  이후. 기본 자격증명(credentials) 정책이 same-origin 으로 변경되었습니다. 파이어폭스는 61.0b13 이후 변경되었습니다.
 

static site 즉 빌드 된 index.html 을 CDN 을 통해서 서빙하지 않으면 이런 장점이 생긴다. nginx 와 같은 웹서버에서 load balancing 을 할때 reverse proxy 를 이용해서 같은 도메인으로 request 를 보낼 수 있게 하면, 로그인 처리를 jwt 와 같은 걸 사용하지 않고도 node.js 의 session 처럼 관리할 수 있다. 다만 이런 경우 ip address 등의 항목이 유실될 수 있지 않도록 설정에 주의하자.

getHydratedApp 함수의 아래 코드가 바로 이런 부분을 처리해주기 위함이다.

<GlobalStateProvider cookies={req.cookies}>
  <App language={language} />
</GlobalStateProvider>

GlobalState 를 cookie 에 넣어주게 되면, 어떤 state 를 서버에서 렌더링 할때도 유용하게 사용할 수 있다. QWER.GG 에서는 default state 를 주입하기 위해 주로 사용한다. geocode lookup 이라거나 i18n 처리 등의 작업도 해당된다.

4. TTFB 를 고려한다.

상당히 많은 과정과 시행착오가 생략되었지만 기본적으로 SSR 은 다음과 같은 순서로 이루어진다.

  1. 기본적으로 필요한 state 를 주입한다.
    theme 설정, geocode lookup, application 구동에 필요한 기본 설정들
  2. route 가 어딘지 확인하고 필요한 데이터를 가져온다.
  • redux 라면 필요한 state 를 주입할테고, (http 나 직접 db 에 접근하거나)
  • graphql 이라면 query 를 execute 해줄것이다.
  1. 데이터와 함께 page 를 렌더링하고
  2. title 이나 meta 등 react root 외에 부분들을 rendering 한다.

이러한 부분을 connection 핸들링 까지 해야 하다보니 react 의 SSR 은 기본적으로 빠를 수가 없다. 모든 route 의 component 를 쪼개서 부분부분 미리 rendering 을 하는 방식이라면 나름대로 가능한 부분도 존재하겠지만, React 의 핵심 코드에 접근해서 마개조를 해야하기 때문에 작은 팀에겐 불가능하다.

또한 CPU 도 많이 쓰고 Memory 도 많이 사용한다. 전체 react 코드를 recursive 하게 훑어야 하다보니 당연히 cpu 의존도가 커지고, 그 과정에서 해당 페이지의 모든 API 에 접근해야 하니 당연히 memory 도 많이 쓰게 된다. 그러다보니 고작(?) SSR 서버 주제에 서버 리소스를 과하게 차지하는 기현상이 생겨난다. 이쯤되면 React 를 쓰는게 너무 괴롭다… 작은 팀에서 감당하기엔 어려운지라 프론트 서버가 너무 맣ㄴ은 리소스를 먹게되면 SSR 을 포기하는 코드를 만들어 두었다.

 
export const renderServerSide = (function() {
  const monitor = monitorSystemUsage(1 * A_SECOND).init();

  return async (req: express.Request, res: express.Response) => {
    if (req.path === '/') {
      return renderDefault(req);
    }

    if (monitor.usage.memory > 70 || monitor.usage.cpu > 70) {
      logger.warn(`System usage exceeds 70%. Skip SSR. Current mem: ${monitor.usage.memory}%, cpu: ${monitor.usage.cpu}%`);

      return renderDefault(req);
    }

    return await renderFromServer(req);
  };
})();

덕분에 지금은 TTFB 가 최대 10초(!!!!!!) 였던 것이 최대 2초로 줄어들었고, 인덱스 페이지 (/) 는 어차피 SEO 에서 의미가 없기 때문에 SSR 을 포기했다. 컨텐츠가 있는 페이즈가 아니고서는 검색엔진에서 가져갈 데이터가 거의 없기 때문이기도 하다.

여전히 2초나 걸리지만, 장족의 발전이다

UA 를 보고 검색엔진인 경우 SSR 해주는 것도 좋은 방법이겠지만, 귀찮아서 당분간은 현재 구조를 유지하기로 했다.


결론

React CRA 를 eject 하지 않고도 충분히 SSR 을 달성할 수 있다. 다만 그대로 사용할 순 없고, 전처리를 해주거나 전처리가 필요하지 않은 styled-component 같은 패키지 (안써봄) 를 사용하면 된다. React 가 개발 생산성 측면에서 매우 뛰어난 것은 사실이나, SSR 이라는 어찌보면 별것 아닌 기능에 많은 리소스가 투여되어야 한다는 점이 무척 아쉽다.

이 글의 서두에 적어놓았듯이 작은 스타트업에게 절대적으로 큰 부분을 차지하는 것이 검색엔진을 통한 유입인데, 개발 편의성을 위해서 비지니스 이점을 포기한다는 것은 장기적인 관점에서 마케팅 비용을 더 크게 지불해야 한다는 것이기 때문에 React 를 위시한 SPA 의 미래가 다소 어둡게 보인다.

 

요즘에는 Phoenix LiveView 나 거기에서 영감을 받은 Laravel Livewire 등도 개발이 되고 있으니 앞으로의 frontend 나아가 웹 개발의 패러다임이 크게 변할지도 모르겠다는 생각이 든다. LiveView 스타일 (? 뭐라고 해야할지도 잘 모르겠다.) 의 웹 개발 방식이 PHP 의 부활을 가져올지도 모르겠다 Elixir 보다는 아무래도 PHP 가 대중적이니 말이다.

기승전라이브뷰

 

출처: https://www.seokjun.kim/react-ssr-the-record-of-pain/

반응형
반응형

https://www.youtube.com/watch?v=lAaD-6OQSHE

 

 

요즘 누군가에 의하면 기술 하나에 꽂혀서 다른 기술은 다 천대하고 있다고 하니, 용기를 갖고 좀 더 꽂혀보도록 하겠다.

Elixir

너무나 아름다운 파이프라인 오퍼레이터...

Elixir 를 좋아하게 된 건 순전히 pipeline 오퍼레이터 때문이었다. 한참 FP 를 해보겠다고 lodash/fp 를 많이 사용할 때였는데, 함수 하나에 여러가지 행동을 하는 코드를 작성하는 곳에 주로 사용하였다. 말하니 복잡한데 다음 코드라고 생각하면 쉽다.

const doSomething = flow( 
  split(‘,’),
  map((x) => x.toUpperCase()),
  filter((x) => x.startsWith(‘A’)),
);
 

한두개 쓸때는 괜찮았는데, 쓰다보니까 flow 를 매번 쓰는게 귀찮았고, 이것저건 관련 자료를 읽다보니 여기까지 도달했다. ES Next 제안 중 하나로, 위 코드와 같은 function chain 에서 |> 를 사용하자는 것이다.

const doSomething = (str) => str
  |> split(‘,’)
  |> map((x) => x.toUpperCase())
  |> filter((x) => x.startsWith(‘A’))
 

flow 는 기본적으로 higher-order function 이라 즉시 사용하려면 const value = flow(a(), b((x) => x), c())(x) 와 같은 구조를 택해야 해서 못생겼다 종종 실수를 했는데 pipeline operator  |>  는 꽤 괜찮은 표현식이라고 생각했다.

This proposal introduces a new operator |> similar to  F# ,  OCaml ,  Elixir ,  Elm ,  Julia ,  Hack , and  LiveScript , as well as UNIX pipes and  Haskell ’s &. It’s a backwards-compatible way of streamlining chained function calls in a readable, functional manner, and provides a practical alternative to extending built-in prototypes.
 

해당 제안 글에서 |> 를 사용하는 언어가 몇가지 소개되었는데 Node.js 말고 새로운 백엔드 언어를 공부해봐야지 하는 와중에 체인파트너스 암호화폐 거래소에 Elixir 를 사용했다는걸 알게되었고, “오?” 하는 마음에 둘러봤더니 패턴 매칭에서 무릎꿇었다 재밌어 보여서 관심을 갖게 되었다. 지금은 언어적인 면에서 패턴 매칭,가드, Atom 등과 같은 Feature 도 매력적이라고 생각하지만, 시작은 어쨋든 |> 이었다.

Phoenix

언어 자체를 공부하는건 나한테는 항상 관심 외의 일이었고, 뭐라도 만들어 보는 것이 Done by doing 이라고 쓰고 맨땅에서 헤딩한다고 읽음 빠른 길이라 생각해 웹 프레임웍을 찾아보게 되었다.

Phoenix Framework 는 Erlang Cowboy 를 기반으로 한 웹 프레임워크

피닉스는 Elixir 의 Ruby on Rails 라고 생각하면 된다. Opinionated framework 고, Elixir 의 개발자 José Valim 은 Ruby 개발자 였고, Phoenix framework 를 만든 Crhis McCord 또한 Ruby on Rails 개발자였기 때문에 여러모로 유사한 아키텍쳐를 가지고 있다. ORM 도 Ruby on Rails 의 그것과 상당히 유사하다. 실제로 Ruby on Rails 에서 LiveView 같은걸 만들다가 concurrency 이슈로 Elixir 로 전향했다 하니  어느정도 그럴만도 하다.

당시에 쓰려다가 포기한 점은 Phoenix 의 ORM 은 Ecto 라는 모듈을 사용하는데 Postgres 가 기본이고 MySQL 도 사용 가능하다. Phoenix 는 새로운 프로젝트를 생성할때 $ mix new [APP] 를 사용하는데, --no-ecto 라는 옵션을 주지 않으면 Postgres 설정 없이 기본 상태에서 서버를 실행할 수도 없다.

 

어릴때부터 나는 “귀찮다” 는 말을 많이 해서 어머니께도 쿠사리 꾸중을 들은 적이 종종 있을만큼 귀찮은 걸 싫어하는데, 그러다보니 새로운 프로젝트는 alter 걱정 없는 MongoDB 를 많이 쓴다. Ecto 는 DB 연결을 위해 Adapter 가 필요한데 MongoDB adapter 가 없고 (정확하게는 있긴 한데 active 하게 개발되진 않는 것 같다), ecto 없이 쓰자니 공부에도 큰 도움이 안될 것 같아 접어두었다

자랑은 아니지만 피닉스 컨트리뷰터고, 예 그렇습니다. 주석 한줄 고쳤구요, 자랑이 맞습니다.

아직까지는 Elixir 라는 언어와 ErlangVM 의 장점을 제외하고 Phoenix 는 Ruby on Rails 에서 많은 영향을 받았다는 것 외에 특별한 개성은 없다고 느껴졌다. 하지만 Phoenix LiveView 는 매력적인 툴이라고 생각한다. LiveView 는 브라우저에서 JS 를 몰아내자 서버에서 렌더링 된 HTML 을 웹소켓을 통해 조작하는 방식이다. 즉 websocket 을 통해 서버에서 state 를 관리하고 render tree 에서 변경된 부분을 browser 에 message 형태로 전달하여 DOM 를 업데이트하는 개념이라고 보면 될 것 이다. 딱 봐도 서버에 부담을 크게 전가될 것으로 보이는데 그게 맞다. 위에서도 언급했듯이 RoR 에서 concurrency 문제로 구현하기 어려웠기 때문에, 동시처리가 강력한 Erlang VM 기반의 Elixir 에서 만들게 된 것이다. 대표적인 SPA 라이브러리인 React 나 Vue.js 를 사용하기 어려웠던 PHP 나 Haskell 등도 영향받아 Laravel LiveWire 라던가 Haskell IHP 와 같은 기술도 개발되고 있다.

하지만 모든 처리를 서버에서 한다는 의미는 browser 의 널럴한 메모리를 거의 사용하지 않는다는 의미와도 연결되는데 file 처리, 이미지 lazyload 나 animation 등 브라우저에서 직접 처리하는 편이 편리한 부분은 LiveView 로 구현이 까다롭거나 거의 불가능한 부분도 분명히 존재한다.

Svelte

LiveView 단독으로 복잡합 인터렉션을 구현하기는 어렵고, 그래서 Frontend View Library 가 필요했다. 이 동네 깡패인 React 를 사용하는 것도 좋은 선택이겠지만, 개인적으로는 React 가 점점 개발하기 어려워 지고 있다고 생각하기 때문에 Svelte 를 선택했다. 가독성을 크게 해치지 않는 선에서 코드가 줄어들면 무조건 이득이라고 생각한다. React 는 hook 이 도입되면서 가독성에 다소 희생이 있었다고 생각하고, Svelte 가 이러한 면에서 더 편한 라이브러리 라고 생각한다.

Phoenix 에 Svelte 를 도압힌다는 부분에서는 좀 더 나아가면 Micro Frontends 라고 하는 괴상한 컨셉과도 연결되는 면이 있다. 물론 단순히 React, Vue, Svelte, Elm 등 원하는 걸 쓰자! 이런 건 아니지만, frontend 의 기술 스택에 대한 자유도를 높일 수 있다는 점에서 접점이 있다고 본다. dev.to 의 Stop Using React 라는 글의 댓글에 보면 이런 말이 나온다.

 
I think this is not the problem with React (well, apart from point 5), but with modern webdev in general. React is a good tool for single-page applications (so is Vue or Angular) but in many cases you simply don’t need and should not have a SPA in the first place.

사실 이게 맞다. 대부분의 경우 SPA (혹은 모던 웹 언어라고 해야 하나?) 는 필요 없다. 한편으로는 정말 복잡한 인터렉션이 필요한 웹 어플리케이션에서는 어차피 느려서 사용할 수 없다. 대부분의 Frontend 는 Data 를 Presentation 하는 역할이 주가 되고, 복잡한 인터렉션이 필요한 부분은 많지 않다. 반대로 복잡한 인터렉션이 필요한 부분에는 SPA 가 최적이라는 뜻이기도 하고.

짤봇 에서는 업로드에서 메모리에 이미지를 들고 있고 여러 처리를 한 후 서버로 리퀘스트를 보내는데, 이러한 일에서는 LiveView 와 같은 방식으로 구현하면 서버쪽 메모리에 부담이 크고 따라서 브라우저에서 처리하는 것이 바람직하다. Svelte 나 React 는 그러한 처리를 하기에 좋은 컴포넌트를 만들 수 있는 Library 지만, React Function Component 의 비동기 처리 방식에서 너무 고통을 받은 터라 Svelte 를 선택했다.

지금 생각해보면 Elixir 와 Elm 이 언어적으로 유사한 부분이 있어서 둘다 마이너여서 종종 함께 언급되는데, elm 쪽으로 가보는 것도 좋은 선택이었을 것 같다. 근데 얘는 진짜 너무 마이너라서 엄두가 안났다.

 

출처: https://seokjun.kim/elixir-phoenix-liveview-svelte/

반응형
반응형

.mjs Vs .js

원래 Node.js의 모듈 시스템은 commonjs를 사용했습니다(require과 module.exports를 사용하는..)

그러다가 ECMAscript 모듈 시스템이 표준이 되었고, Node.js는 이를 지원하게 되었습니다.

Node.js 는 .cjs 파일로 commonjs 모듈 시스템을 지원했고,

.mjs파일로 ECMAsript 모듈 시스템을 지원했습니다.

그러고 js파일은 둘 다 모두를 지원하게 되었습니다.

(default는 commonjs)

(package.json에 "type" :"module"쓰면 ecma 모듈 지원)

 

 

.JS

const b = require("./main2.js");

console.log(b.a);

받아오는 js

function hello() {
  return "hello";
}
const a = "a";
module.exports = {
  hello: hello,
  a: a,
};

보내주는 js

 

.mjs OR package.json에 타입 추가

import { hello, a } from "./main2.mjs";

console.log(`${hello()} ${a}`);

받아오는 js

function hello() {
  return "hellozz";
}
const a = "a";

export { hello, a };

보내주는 js

 

출처ㅣ https://kjs-dev.tistory.com/entry/JS-js-%EC%99%80-mjs?category=1025503?category=1025503

반응형

'Web > Js' 카테고리의 다른 글

ES6 Map(), Set()  (0) 2023.01.19
React 18의 useSyncExternalStore, Tearing 현상은 무엇인가?  (0) 2023.01.18
useEffect와 useLayoutEffect  (0) 2023.01.11
React.memo와 useMemo 차이점  (0) 2023.01.11
[React]setState Callback 함수 사용  (0) 2023.01.11
반응형

zustand : 독일어로 상태

react state-management " zustand

상태관리가 왜 필요한가

사용자와의 활발한 인터랙션을 모두 처리할 수 있으며, 실시간 통신을 통해 데이터를 받아오고 이를 기반으로 렌더링 하는 등 다양한 기능을 포함하고 있다. 그렇지만 기반이 되는 핵심은 애플리케이션 수준에서는 자체적으로 상태를 관리해야하는 역할이 필수적으로 요구된다는 점이다.

React에서 상태란 오늘날 웹 프론트엔드 개발에 있어서 상태라는 것은 크게 보면 웹 애플리케이션을 렌더(render)하는데 있어 영향을 미칠 수 있는 값이라고 한다.

Plain Javascript Object hold information influences the output of render ___ 공식문서

리액트는 독립적인 컴포넌트 단위로 구성되어있다. 하나의 단위에서만 생활하던 때는 상태라는 것이 쉽게 공유되었다. 부족단위, 마을단위, 학교단위 등 단위 내 상태는 마이크하나로 전파 될 수 있었다. React 에서는 hook useState를 사용해 하나의 컴포넌트에서 충분히 상태를 관리했고 인근 옆 단위까지도 훌륭히 상태 전파의 기능을 수행했다.

여기서 주목해야할 점은 독립이다.

하나의 독립적인 컴포넌트 단위에서의 공유는 쉬웠는데, 이젠 그렇지 않은 시대가 되었다. 글로벌시대에 글로벌하게 상태를 관리 할 수는 없을까라는 의문이 생기기 시작했다.

React에서는 자체적으로 상태를 관리할 수 있도록 여러 기능을 제공하고 있다. 하지만 사용자의 입장에서는 효율적으로 전역 상태를 관리해야하는 필요가 생겨났다. 그래서 등장한 것이 상태관리 라이브러리다.

redux / Context API / Mobx / Recoil / zustand / etc

많은 상태관리 라이브러리가 있지만, 이번 프로젝트에는 zustand를 사용해보고자 한다.

다시 zustand🥗

A small, fast and scalable bearbones state-management solution using simplified flux principles. Has a comfy api based on hooks, isn't boilerplatey or opinionated.
간단한 Flux 원칙을 사용하는 작고 빠르고 확장 가능한 상태 관리 솔루션입니다. Hook 기반으로 하는 편리한 API가 있습니다.

zustand를 사용하고자 마음먹은 건 한 블로그의 제목을 보고서였다.

개쉽다! Zustand 사용법 - React 상태관리 라이브러리

개쉽다는데 안 써볼 이유가 없었다. 내가 사용해본 상태관리 라이브러리는 Recoil뿐인데 사용한 이유도 리액트 Hook 과 비슷해 쉽다고 들어서였다. 짧은 프로젝트 기간에서 효율을 내기위해 낮은 러닝커브의 라이브러리를 선택했다. 이번엔 그냥 쉬워서라기보다 장단점을 고려해 zustand를 사용해보고자 한다.

Zustand는 Context API 사용을 배제하고 클로저를 활용하여 스토어 내부 상태를 관리한다. 따라서 createStoreHook 을 호출하여 리턴된 useStore 를 어느 컴포넌트에서나 import하여 원하는대로 사용하더라도 같은 스토어를 바라보게 된다.

flux 기반

기존 MVC 패턴

Model : 저장소
Controller : Model의 데이터 관리 (CRUD)
View : 받아온 Model의 데이터를 사용자에게 보여줌

페이스북은 “MVC는 정말 눈 깜짝할 사이에 복잡해진다”고 말하며 이 문제의 해결 방안으로 단방향 데이터 흐름을 가지는 Flux 패턴을 고안했다고 한다.


장점강화 Flux 패턴

Flux는 사용자 입력을 기반으로 Action을 만들고 Action을 Dispatcher에 전달하여 Store(Model)의 데이터를 변경한 뒤 View에 반영하는 단방향의 흐름으로 애플리케이션을 만드는 아키텍처 예시

각 요소들은 단방향 흐름에 따라 순서대로 역할을 수행하고, View로부터 새로운 데이터 변경이 생기면 처음부터 다시 이 순서대로 실행합니다. 이렇게 함으로써 예외 없이 데이터를 처리할 수 있게 된다고 한다. 아무튼 그렇다고 한다.

간단한 장점

  1. 굉장히 쉽다. 동작을 이해하기 위해 알아야 하는 코드 양이 아주 적다. 핵심 로직의 코드 줄 수가 약 42줄밖에 되지 않는다. (VanillaJS 기준)
  2. 보일러플레이트가 거의 없다. (Context API랑 비교)
  3. redux Devtools를 사용할 수 있어 debugging에 용이하다.
  4. 상태 변경 시 불필요한 리랜더링을 일으키지 않도록 제어하기 쉽다.

zustand 사용법👩🏽‍💻

설치

npm install zustand
yarn add zustand

상태를 관리하는 store 만들기

// store.js

import create from "zustand";
import { devtools } from "zustand/middleware";

// set 함수를 통해서만 상태를 변경할 수 있다
const useStore = create(
    devtools((set) => ({
        isLogin: false,
        toggleIsLogin: () => set((state) => ({ isLogin: !state.isLogin })),

        count: 1, //state

      
      	// set 함수 사용 #1 현재 상태를 기반으로 새로운 상태를 리턴하는 함수
        increaseCount: () => {
            // count 1만큼 증가
            // set method로 상태 변경 가능
            set((state) => ({ count: state.count + 1 }));
        },

        // set 함수 사용 #2 아예 변경하려는 상태 값
        setCnt: (input) => {
            // 입력받은 input만큼 count 설정
            set({ count: input });
        },

      

        clearCnt: () => {
            // count 초기화
            set((state) => ({ count: 0 }));
        },
    }))
);

// redux devtools 사용하기
// const useStore = create(devtools(myStore));

export default useStore;
  1. store.js javascript 파일 만들기
    나는 src / status / store.js 파일을 만들었다.
  2. 제일 상단에 zustand의 create를 불러와준다. 그래야 사용할 수 있다.
    import create from "zustand";
  3. store를 생성한다.
  • 스토어는 Hook 이다.
  • 상태를 통합적으로 관리한다. 신병훈련소 느낌
  • 어떤 type이든 넣을 수 있다. (원시, 객체, 함수)
  • set함수는 상태를 병합한다.
// store hook 생성
const useStore = create(
    (set) => ({
        
      isLogin: false, //state
      toggleIsLogin: () => set((state) => ({ isLogin: !state.isLogin })),
      
    })
);

export default useStore;

먼저 상태가 진짜 관리 되는지 확인 차 boolean type의 객체로 버튼 클릭 시 토글되는지 실험을 해보았다.

isLogin state에 false를 담아놨다.
toggleIsLogin state에는 함수를 넣고 set함수를 넣어 isLoginstate 상태를 호출될 때마다 반전시키도록 했다.

ex)
toggleIsLogin -> 호출 되면 -> isLogin : true
toggleIsLogin -> 호출 되면 -> isLogin : false

컴포넌트에 바인딩한다. 살포시 적용

store를 컴포넌트에 불러오는 방법은 2가지가 있다.

	// #1 select 함수를 사용해 import 하여 사용하기
     const isLogin = useStore((state) => state.isLogin);

	// #2 구조분해 할당을 통해 가져오기 
     const { isLogin } = useStore();

직관적인 방법은 #2 구조분해 할당을 통해 가져오는 방법인 것 같다.

const App = () => {

    const { isLogin, toggleIsLogin } = useStore();

    return (
        <div>
            <p>{"" + isLogin}</p>
      		<button onClick={toggleIsLogin}>
                <b>버튼 클릭 시 백만원</b>
            </button>

        </div>
    );
};

아주 쉽게 위 블로거가 말한대로 개쉽게 전역상태관리를 적용하였다.

조금 더 적용해보기

김관장 블로거님의 zustand 글 참고해서 추가로 구현해보고자 했다.
근데 그대로 복붙해서도 안되는 것이었다. 그래서 공식문서를 참고했다.
set함수를 사용할 때 괄호를 잘 써야 오류가 안난다. 중괄호 대괄호가 많기 때문에 주의하기!

//store.js

const useStore = create(
    devtools((set) => ({
        isLogin: false,
        toggleIsLogin: () => set((state) => ({ isLogin: !state.isLogin })),

        count: 1, //state

      	// 함수를 전달하여 상태를 갱신하는 경우
        increase: () => {
            // count 1만큼 증가
            // set method로 상태 변경 가능
            set((state) => ({ count: state.count + 1 }));
        },

      
      	// 객체를 직접 전달하여 상태를 갱신하는 경우
        setCnt: (input) => { //매개변수를 입력받아,
            // 입력받은 input만큼 count 설정
            set({ count: input });
        },

        clearCnt: () => {
            // count 초기화
            set((state) => ({ count: 0 }));
        },
    }))
);
//app.js


const App = (props) => {
    const { isLogin, count, increase, toggleIsLogin, setCnt, clearCnt } =
        useStore();

    return (
        <div>
            <div>
                <p>{"" + isLogin}</p>
                <button onClick={toggleIsLogin}>
                    <b>버튼 클릭 시 백만원</b>
                </button>
            </div>
            <div>
                <div>현재 Cnt == {count}</div>
                <button onClick={increase}>[+1]</button>
                <button onClick={() => setCnt(10)}>[set_10]</button>
                <button onClick={clearCnt}>[clear]</button>
            </div>
        </div>
    );
};

아름답게 잘동하는것을 볼 수 있다. 이정도 기능은 사실 useState로도 충분하고 차고 넘치지만 어디에서나 사용할 수 있는 상태라는 점에서 맘에 들고 무려 Recoil보다 사용방법이 쉬웠다.

작동원리

출처

redux-devtool을 사용해보자

Zustand는 Middleware로 Devtools를 지원한다.
Redux DevTools를 Chrome 웹 스토어에서 설치하고 store를 devtools 로 묶어 주면된다.

// store.js
// redux devtools 사용하기

import create from "zustand";
import { devtools } from "zustand/middleware"; // #1 import 하기

const useStore = create(
    devtools((set) => ({ // set을 devtools로 묶어주기
       ...
    }))
);


// const useStore = create(devtools(myStore)); // 이렇게 해도 된다는데 난 안되더라...

export default useStore;

참고링크

 

출처: https://velog.io/@ho2yahh/react-%EC%83%81%ED%83%9C%EA%B4%80%EB%A6%AC-Zustand-%EB%A5%BC-%EC%95%84%EC%8B%AD%EB%8B%88%EA%B9%8C

반응형

+ Recent posts