본문으로 건너뛰기

"react" 태그로 연결된 12개 게시물개의 게시물이 있습니다.

모든 태그 보기

aggrid 디버깅

· 약 1분

AGGRID

  • 풀스펙을 지원하는 그리드
  • 대부분의 기능들은 엔터프라이즈 모듈로 분기되어있다.

모듈정보

  • Modules
    • ag-grid-react는 통 패키지
    • @ag-grid-community, @ag-grid-enterprise 스코프 하위는 모듈
  • Modules - Example

데이터 페칭

  • onGridReady 이후 실행
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IOlympicData[]) => setRowData(data));
}, []);

코드 예시

  • Full Spec
  • 여기서 보는 게 낫다. 문서가 최신화 되어있지 않다.

아톰 기반 상태 관리와 Valtio

· 약 4분

개요

  • 한 때 redux 로 지친 상태관리를 해결하기 redux-saga 가 멋져보였고, 그 다음엔 원자 상태들의 조합이 그 해결방법으로 보였다.
  • 그래서 recoil 을 주로 사용했지만, query 나 swr를 사용한다면 로컬 UI 상태를 관리하는데에 그렇게 복잡한 기능이 필요하지 않다.
  • recoil 은 메타에서 버그의 수정만 되는 거의 버려진 프로젝트로 보인다. discussion#2171
  • 어차피 로컬 UI 상태는 pub/sub 패턴을 가지고 있기만 하면 된다.
  • 2023년의 상태관리 라이브러리의 우승자는 zustand 로 보인다.
    • 이벤트/메세징 기반 시스템을 구성한다면 Flux 패턴을 가진 해당 라이브러리를 사용하는 게 맞다.
    • 프론트엔드의 모든 기능을 액션 기반으로 동작하게 하여 에디터를 개발하던가 채팅시스템에서부터 기능호출 요청을 받아 연동한다던가하는 작업이라면 말이다.
  • 하지만 일반적인 유스케이스에서는 계단식 폼, 뱃지, 모달 등을 제외하면 Flux는 과하다.
  • 혼자 개발하는데 백엔드와 프론트엔드를 분리하고, Local, Dev, Stage, Prod 스테이지를 분리하면서 DB, 스케쥴러, 큐, API, SSR 스택도 모두 가져가려는 듯한 욕심이다.

atom

  • 번역, 리액트 상태 관리의 새로운 흐름을 보자.
  • 원자 단위면서 pub/sub, derived 가 가능하면 된다.
  • derived state 를 사용하는 것 또한 id 별로 새로운 상태가 필요한 경우가 아니라면 거의 없었다. 데이터로는 이미 query 가 그 역할을 하고 있다.
  • 아토믹 디자인, 아토믹 상태관리는 필수가 되었다.
  • 많은 개발자가 비동기적으로 개발하는 상황에서는 어떤 파일이 아톰인지, 뭘 위한 아톰인지를 기록하는 게 중요했다.
  • *.atom.ts 파일 서픽스와 *State 변수명 서픽스를 갖게 했다.

valtio

  • jotai/atom, Recoil/atom 모두 아톰기반의 상태를 관리하고 상태를 전파한다.
  • valtio/proxy 는 Proxy와 Reflect를 사용해 같은 기능을 구현했다.
  • Results for js web frameworks benchmark를 참조해보면 메모리 관리가 최악은 아니다.
  • zustand, jotai, valtio 모두 pmndrs 커뮤니티에서 다루는 오픈소스이고 같은 사람이 컨트리뷰터, 메인테이너이다.
  • valito/discussion#128 에서의 그의 의견처럼 그냥 작동한다.
  • valito/issues#141 Redux, Redux-saga 로 1년, Mobx POC, Recoil 로 2년을 보낸 나에겐 너무나 공감이 되는 코멘트였다.
  • 당분간은 여러 프로젝트를 만들어보면서 모두 이 상태관리 라이브러리를 사용해보려고한다.

Nextjs App Directory

· 약 4분

개요

  • nextjs 13버전에서 app 디렉토리가 생기면서 client/server 컴포넌트를 쉽게 사용할 수 있는 환경이 되었다.
  • getStaticProps, getServerSideProps 와 같은 메소드가 사라졌고, 양쪽이 fetch 로 통합이 되었다.
  • 페이지별 상태 초기화를 위한 HOC 중첩을 가져가지 않아도 될 것 같았다.

App

Node.js + React DOM Renderer

app 내의 모든 로직은 Node.js 이다. 따라서 이런 로직이 가능하다.

app/page.tsx
import { hostname } from "os";

export default function Main() {
return <div>{hostname()}</div>;
}

이벤트를 바인딩할 수 없다.

app/page.tsx
import type { SyntheticEvent } from "react";

export default function Main() {
const handleSubmit = (event: SyntheticEvent<HTMLFormElement>) => {
console.log("submitting");
};

return (
<form onSubmit={handleSubmit}>
<button type="submit">submit</button>
</form>
);
}
Error: Event handlers cannot be passed to Client Component props.
<form onSubmit="{function}" children="...">
^^^^^^^^^^ If you need interactivity, consider converting part of this to a
Client Component.
</form>

Design system

대부분의 디자인 시스템 라이브러리는 ThemeProvider 로 테마 상태를 공유하고 Baseline StyleSheet를 전역에 넣어준다. 스타일시트를 위해 RootStyleRegistry 란 HOC 만들어 Baseline StyleSheet 를 동적으로 넣어주면 초기화는 가능하지만,

문제는 Server Component 내에서 use 을 사용할 수 없으니 ThemeProvider 로 기능동작이 불가능하다. 어찌저찌 'use-client' directive 로 Client Component 로 설정한다고 하여도 Provider 로 인해 하위 모든 컴포넌트가 Client Component 로 동작해야할 것이다.

그래서 문서에서 다음과 같이 표기가 된 것으로 보인다.

If you want to style Server Components, we recommend using CSS Modules or other solutions that output CSS files, like PostCSS or Tailwind CSS.

위 방식으로 mui/material-ui/examples/material-next-app-router-ts 예시가 추가되었지만, 이렇게 쓸바에 pages 폴더 라우트와 다를 게 없다고 생각한다.

결론

  • 아직 app 폴더를 사용하기엔 이르다.
  • vercel/app-playground에 충분한 예시가 갖춰지고, 생태계가 React Server Component 를 충분히 지원할 때까지는 프로덕션에서 사용은 불가능하다고 보인다.

사견

개인적으로는 왜 이렇게 많이 클라이언트에서 렌더링해야해? 그냥 필요한 영역만 클라이언트에서 그려주면 되잖아? 라는 접근방식은 디버깅 관점에서 마음에 들진 않는다. 웹을 하나의 Client 관점에서 본다면, 설치의 유무만 다를 뿐 앱에서 서버에서 렌더링된 HTML을 받고 일정부분만을 Server Driven UI 로 가는거와 마찬가지다. 복잡도가 크게 증가한다.

이러한 시도는 이미 Dotnet, Laravel Livewire 등 다른 언어에서 많이 진행되어왔고, Remix 에서는 이미 잘 동작 중이다. 단지 React 를 서버에서 쓰기 위해 다시 MVC 시절로 회귀하려는지 모르겠다.

Flutter와 타 프레임워크 비교

· 약 1분

비교

-FlutterReact NativeIonic
언어Dart + FlutterJS + ReactJS
컴파일결과네이티브 앱부분적인 네이티브앱웹뷰
UI 컴포넌트 컴파일🙅‍♂️ (픽셀제어)🙆‍♂️🙅‍♂️
플랫폼모바일, 웹, 데스크탑모바일, 웹모바일, 웹, 데스크탑
벤더구글페이스북아이오닉

요약

  • 플러터는 화면의 픽셀을 직접 제어한다.
  • 웹으로 빌드시에 캔버스 위에 생성된다.

react에서 scrollTo 메소드로 스크롤 이동이 안될 때

· 약 2분

window.scrollTo(0, 0) 를 아무리 해봐도 스크롤이 맨 위로 올라가지 않을 때 많은 구글링을 한 뒤 다음과 같은 해결책을 적용해보았다

  • react-scroll 라이브러리를 써보고
  • 혹시 window 가 레이아웃 컴포넌트에 갇혀있어서 그런가 window 를 App.js 에서 Top 기능을 쓰는 곳까지 내려보고
  • jQuery 를 추가해서도 해보고
  • 별 짓을 다하다가

아무 것도 안 되서 리프레쉬하러 옥상에 갔다가 갑자기 든 생각이 있었다

잠깐만 component 라는게 하나의 element 안에서 쇼를 하는 거 잖아. 그럼 element 안에서 스크롤을 이동하면?

Footer.js
class Footer extends Component {
scrollToTop = (event) => {
document.getElementById("root").scrollTo(0, 0);
};

render() {
return (
<div>
<div className="top">
<a onClick={this.scrollToTop}>위로가기</a>
</div>
</div>
);
}
}

index.js 에서 ReactDOM 을 렌더링해주는 element(나같은 경우엔 div#root)를 찾아서 그 엘레먼트의 스크롤을 올려주자

여담

책에서 봤는데 뇌가 쉴 때 가장 좋은 아이디어가 나온다고 했는데, 사실인 것 같다

redux-form 사용하기

· 약 9분

react 로 form 을 만들어 사용하는 일은 생각보다 많은 걸 해야한다.

  • onSubmit에 이벤트를 잡아서 event.target.fieldName.value로 하나씩 체크를 해서 validating 을 하다가
  • 귀찮아질 때 쯤 function 을 만들어서 관리를 하고
  • 자주 사용하는 form input 을 컴포넌트로 만들며
  • form 에 초기값을 넣어줘야할 경우 (edit 페이지 같은) 값을 가져와 하나하나 바인딩하고
  • input 에 값이 바뀌었는지에 따라 submit 버튼을 활성, 비활성화 하는 로직도 있어야한다

번거로운 일을 하는 것 보다 redux-form 라이브러리를 사용해보자 검색시에 나오는 포스트들은 아주 기본적인 예제 밖에 없어서 문서를 매번 헤집는 시간이 필요했다.

먼저 이 라이브러리를 쓰기 위해선 다음 사전 지식이 필요하다

$ yarn add redux-form

redux-form 의 리듀서를 연결시켜줘야한다

rootReducer
import { reducer as formReducer } from "redux-form";

const rootReducer = combineReducers({
// form 키를 사용해야한다
form: formReducer,
});

export default rootRecuder;

사용

redux-form component 와 redux-form 이 들어갈 compoent 를 만들어야한다 편의상 Login componentLoginForm component라고 하자

Login
import React, { Component } from "react";
import { connect } from "react-redux";
import LoginForm from "LoginForm";

class Login extends Component {
submitLoginForm = (formData) => {
console.log("LoginFormData => ", formData);
// this.props.loginAuth(formData)
};

render() {
return <LoginForm onSubmit={this.submitLoginForm} />;
}
}

export default connect()(Login);

LoginComponent에서는 딱히 특별한게 없이 LoginForm Component 를 호출하고 함수 하나를 내려준 게 끝이다

LoginForm
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";

class LoginForm extends Component {
render() {
const {
handleSubmit, // submit 이벤트를 redux-form에서 관리하게 수정
pristine, // form이 변경점이 없을 경우 true
submitting, // form이 submit 중일 경우 true
} = this.props;

return (
<form onSubmit={handleSubmit}>
<Field
name="id"
type="text"
component="input"
placeholder="아이디를 입력하세요"
/>
<Field
name="password"
type="password"
component="input"
placeholder="비밀번호를 입력하세요"
/>
</form>
);
}
}

export default reduxForm({
form: "LoginForm", // formReducer에 어떤 이름으로 이 폼을 등록할지
})(LoginForm);

redux-form에서 Field ComponentreduxForm High Order Component를 가져온 뒤 input tag 대신 Field Component 의 component 속성으로 input 을 만들어준다 그리고 해당 폼을 reduxForm 으로 감싸 redux 와 연결한다

handleSubmit은 onSubmit prop 으로 들어온 함수를 폼과 연결해준다 (event.preventDefault() 및 폼의 필드들을 serialize 해서 object 로 바꿔 onSubmit 함수의 parameter 로 넘겨준다)

말이 좀 어렵다면 Login.jssubmitLoginFormconsole.log(formData)의 구조를 보자

{
"id": "idValue",
"password": "passwordValue"
}

오 꽤 나이스하다 하지만 필드만 덩그라니 있는 경우는 없다 더 업그레이드 해보자

스타일이 필요한 필드

보통의 필드들은 label이 들어가 있고, selectBox, checkBox의 경우엔 스타일을 주기위해 대부분 외부 라이브러리를 쓴다 이럴 때 Field Component 의 component 속성에 원하는 모양을 만들어서 넣어주면 된다

LoginForm
...

const renderInputField = ({ input, type, label, placeholder, meta: { touched, error }}) => {
return (
<div>
<label>{label}</label>
<input {...input} type={type} placeholder={placeholder} />
{ touched && error &&
<span className="error">{error}</span>
}
</div>
)
}

class LoginForm extends Component {

render() {
const {
handleSubmit,
pristine,
submitting
} = this.props

return (
<form onSubmit={handleSubmit}>
<Field
name="id"
type="text"
label="아이디"
component={renderInputField}
placeholder="아이디를 입력하세요"
/>
<Field
name="password"
type="password"
label="비밀번호"
component={renderInputField}
placeholder="비밀번호를 입력하세요"
/>
</form>
)
}
}

renderInputField라는 함수형 컴포넌트를 만들어서 Field.component prop 에 넣긴했는데, div>input을 렌더링 해주는 것만 보이고 엄청난 파라미터들 때문에 정신이 아득할 것이다

component prop 에 함수형 컴포넌트를 넣으면 field prop 을 받게 된다 여기엔 내려준 prop 뿐 아니라 reduxForm 에서 넣어준 prop 도 존재한다 원래 해당 렌더링 컴포넌트는 아래와 같은 모습이다

const renderInputField = (field) => {
return <div>...</div>;
};

그럼 먼저 reduxForm 에서 넣어준 prop 을 보자

  • field.input: input 은 모든 HTML input attribute 를 가지고 있는게 아니라 다음 몇가지만 갖고 있다
    • checked
    • name
    • onBlur
    • onChange
    • onDragStart
    • onDrop
    • onFocus
    • value
  • field.meta: meta 는 해당 input 의 상태에 관한 데이터가 있다
    • touched: 해당 input 이 한 번이라도 클릭이 되어졌는지
    • valid: 해당 input 값이 valid 한지
    • error: 해당 input 값에 에러가 있으면 error message 가 들어온다
    • ... 나머진 문서를 참조하자

이제 type, label, placeholder 는 field.input prop 에 없어서 직접 넣어줬고, meta 는 이미 정의되어 있었구나 란 renderInputField 함수의 props 가 보일 것이고 Field Component 의 구조를 마음대로 변경할 수 있게 되었다

동적으로 value 를 바꿔야할 때

checkbox, select 의 경우엔 스타일이 들어가면 element 의 onClick 를 잡아서 hidden field 의 데이터를 바꿔줘야한다 checkbox 전체 영역을 클릭할 때마다 기존의 값을 toggle 해주는 component 를 만들 수 있다

LoginForm
const renderCheckboxField = ({ input, label }) => {
return (
<div onClick={(event) => input.onChange(!input.value)}>
<i
className={`fa ${!input.value ? "fa-square-o" : "fa-check-square-o"}`}
/>
<label>{label}</label>
<input type="hidden" {...input} />
</div>
);
};

value 에 따라서 font-awesome icon이 변경된다

함수로 값을 변경해야할 경우가 있을 수도 있다 이럴 땐 reduxForm prop 의 change 메소드를 사용하면 된다 바꿀 input 의 name 속성과 value 값을 넘겨주자

changeInputValue = (targetInputName, val) => {
this.props.change(targetInputName, val);
};

초기값

로그인 폼에는 아이디 저장 기능을 붙히면 폼이 초기화될 때 그 값을 가져와야한다

Login
...

class Login extends Component {
state = {
userId: ''
}

componentDidMount() {
const userId = localStorage.getItem('userId')

this.setState({
userId
})
}

render() {
return (
<LoginForm
onSubmit={this.submitLoginForm}
initialValues={{
id: this.state.userId
}}
/>
)
}
}

Login Component 에는 초기값을 가져와서 initialValues prop 안에 넣어준다

LoginForm
class LoginForm extends Component {
render() {
return <form>...</form>;
}
}

export default reduxForm({
form: "LoginForm",
// 이 값은 LoginForm 컴포넌트가 로드되고 나서
// initialValues가 바뀔 경우 폼 값도 업데이트가 되야되는지의 여부이다
enableReinitialize: true,
})(LoginForm);

localStorage.userId에 값이 들어있다면 그 값으로 id Field 가 초기화 된다 컴포넌트가 로딩되기 전에 값이 정해져있다면 enableReinitialize 옵션이 없어도 되지만, 수정 폼처럼 기존 데이터 fetch 후 또는 componentDidMount 후에 값을 가져온다면 해당 옵션값을 true로 넣어줘야한다

validation

여기까지 이해했다면 validation 은 큰 어려움 없이 문서의 예제소스를 참조하면 붙힐 수 있다

여담

react 라이브러리들은 읽은 뒤 바로 적용하긴 힘들다고 생각한다 매번 삽질이 필요한듯

react-intl로 번역 적용하기 (react i18n)

· 약 3분

리액트 앱에서 번역기능을 사용하려면 react-intl 패키지를 사용하면 된다. 근데 Documentation을 보면 format 기능들, HOC로 데이터를 넣는 등 여러 기능 덕에 정신이 아득하다

step by step으로 간단하게 적용해보자

npm install react-intl

번역 데이터 생성

편의상 root에 locale.js로 만들었다. 서비스시엔 locale 폴더에 언어별로 파일을 나눠 관리하자.

locale.js
export default {
en: {
hello: "Hello",
},
ko: {
hello: "안녕하세요",
},
ja: {
hello: "こんにちは",
},
};

연동

index.js
import { IntlProvider, addLocaleData } from "react-intl";
// 이 서브 라이브러리들이 내 locale 파일을 사용할 수 있게 해준다
import en from "react-intl/locale-data/en";
import ko from "react-intl/locale-data/ko";
import ja from "react-intl/locale-data/ja";
import locale from "./locale";

addLocaleData([...en, ...ko, ...ja]);

// 저장되어 있는 언어 데이터를 가져온다
const defaultLang = localStorage.getItem("lang") || "en";

ReactDOM.render(
<Provider store={store}>
<IntlProvider locale={defaultLang} messages={locale[defaultLang]}>
<App />
</IntlProvider>
</Provider>,
document.getElementById("root"),
);

사용

import { FormattedMessage } from "react-intl";

<FormattedMessage id="hello" />;
// 저장되어 있는 언어 값에 따라 Hello, 안녕하세요, こんにちは 중 하나가 보여진다

inject

placeholder 등에서 텍스트만 필요할 때 component를 사용하지않고 다음과 같이 intl을 주입해서 사용한다.

import React, { Component } from "react";
import { injectIntl } from "react-intl";

class SignupForm extends Component {
render() {
const { intl } = this.props;

return (
<form>
<input
type="text"
name="id"
placeholder={intl.formatMessage({
id: "hello",
})}
/>
</form>
);
}
}

export default injectIntl(SignupForm);

child

HOC를 사용하지 않을 경우 FormattedMessage의 child로 번역된 문구를 받으면 된다

<FormattedMessage id="hello">
{(hello) => <input type="text" name="id" placeholder={hello} />}
</FormattedMessage>

dynamic

Hello, {Gracefullight} 처럼 동적으로 문구가 변해야할 경우valuesprop을 활용하자 먼저,locale.js에서 변수가 될 부분을 로 감싸준다

locale.js
export default {
en: {
helloUser: "Hello {user}",
},
ko: {
helloUser: "{user} 안녕하세요",
},
ja: {
helloUser: "{user} こんにちは",
},
};

values 아래 object로 변수 값을 넣어주면 된다

<FormattedMessage
id="helloUser"
values={{
user: "Gracefullight",
}}
/>

여담

mo, po 파일 건들던 시간들이 너무 아깝다

리액트 네이티브 윈도우 네트워크 설정 (react-native)

· 약 2분

리액트 네이티브로 개발한 앱을 Expo로 구동 중 network response timed out 에러 메세지가 보인다면 다음과 같이 해결하면 된다.

인바운드로 19000, 19001 포트를 열어줘야한다. 방화벽 > 고급 설정 > 인바운드 규칙 > 새 규칙 > 포트 에서 아래와 같이 설정해주고 이름은 react-native-expo 등 원하는 이름으로 규칙을 생성하자.

image from hexo

Hostname 설정

아이피 확인

ipconfig로 현재 Wifi가 설정된 내부 아이피 주소를 확인한다.

무선 LAN 어댑터 Wi-Fi:
연결별 DNS 접미사. . . . :
링크-로컬 IPv6 주소 . . . . :
IPv4 주소 . . . . . . . . . : 192.168.0.7
서브넷 마스크 . . . . . . . : 255.255.255.0
기본 게이트웨이 . . . . . . : 192.168.0.1

192.168.0.7 을 기억해 놓자.

설정 추가

REACT_NATIVE_PACKAGER_HOSTNAME 환경 변수를 설정해줘야한다.

Git Bash 환경에서는 다음과 같다.

export REACT_NATIVE_PACKAGER_HOSTNAME=192.168.0.7

실행

yarn start 후에 Expo 앱으로 QRCode를 찍으면 정상적으로 실행이 가능하다.

왜 리덕스 사가(Redux-saga) 인가?

· 약 13분

redux, redux-thunk, redux-promise-middleware, redux-actions, redux-saga 머리가 뽀개질 지경이다. 결국엔 redux-saga를 써야만 했고 왜 saga로 수렴하게 되는지에 대한 삽질기다.

도대체 몇 개의 미들웨어 라이브러리를 파야하는지 화남을 포스팅했기에 오 프론트엔드를 꾸미는데는 리액트가 최고야 라고 생각하는 분들에겐 마음이 안들 수도 있다. 정신건강을 위해 mobx 사용을 추천드립니다.

redux

하위, 상위 컴포넌트에 데이터를 props로 넘겨주는게 너무 관리하기 힘들어서 선택한다. 하나의 Store(Object)에 SPA의 모든 데이터를 보관한다. 대안으로는 Event Bus Component를 만들면 되는데, 하나의 스토어를 갖는게 그게 그거라 거의 강제된다. redux를 적용하기에 앞서 기존 MVC 패턴에 쩌든 사상을 Flex(단방향) 패턴으로 바꾼다는게 거의 남북간 화합 수준이였다. 이 부분에 있어선 연습만이 답이다. 눈감고 코딩할 수 있을 만큼 예제 프로젝트를 반복해보자. 일단 몸에 익으면 action creator가 뭔지 왜 dispatch를 해야하고 reducer로 값을 처리해야 하는지가 뇌에서도 이해가 되는 느낌이였다. (counter 예제는 좋지 않다고 본다. shopping cart예제나 todo 예제가 더 이해하기 쉽다.)

redux-promise-middleware

redux에서 action의 payload를 비동기 데이터로 넘길 때를 알아서 처리해준다. 많은 포스팅이 있는데, 별 쓸모 없다. 실무에서 예제 포스팅을 따라할만한 간단한 비동기 처리 로직은 없다.

redux-thunk

하나의 action에서 여러 개의 다른 action을 호출하던지, action이 dispatch되는 걸 조작할 수 있어, 비동기 처리에도 사용한다. 멋진 라이브러리이지만 클로저 패턴을 사용해야하므로 소스가 지저분하다. (이게 깔끔하다고 생각하면 쓰면 된다. promise then promise then chaning도 깔끔하다고 생각하다면)

redux-actions

action, type, reducer의 반복되는 코드를 더 깔끔하게 구현하길 원한다면 이게 답일 수 있다. 하지만 이 미들웨어는 구문적인 편의성을 제공하는 것일 뿐 비동기를 처리할 수 없어서 promise, thunk 또는 여기서 소개할 saga가 강제된다. 여기를 참조하자.

rematch

1803 최근엔 이 모든 고민을 해결해주는 rematch 라는 라이브러리가 있다 async/await를 사용해 간결한 문법을 제공하는데 아직 삽질해보지 않아 소개정도만 하고 지나가겠다

redux-saga

어떤 action은 promise payload를 뱉고, 어떤 action은 dispatch로 다른 action을 호출하고 어떤 action은 plain object를 뱉고 이런 일관성없는 짓을 계속하다보면 정말 비효율적이라는 느낌을 받는다. 안 받으셨다면 해보시면 느껴질 것이다.

더 극단적으로 든 생각은 Vuex를 사용하면 그냥 mapGetters, mapActions 두 함수만으로 직관적이고 디자이너도 알아볼 법한 코드로 구현이 가능한데 왜 react를 해야되지? 란 의문이 계속 들었다.

Redux-saga를 접하고는 드디어 state 관리의 친구를 만난 느낌이였다. 그렇다면 saga는 뭘까? 한 줄 표현만 기억해보자.

saga란 action에 대한 listener이다. 음.. 액션 리스너구나. 이벤트 리스너같은 것이군.

설치를 하기 전에 reduxgenerator의 개념을 완벽히 이해했다고 가정한다.

설치

# yarn add redux react-redux
$ yarn add redux-saga

create-react-app으로 생성된 프로젝트로 폴더 구조를 다음과 같이 가져갔다.

.
├── public
└── src
├── components
├── containers
└── store
├── actions
├── reducers
├── sagas
└── types
├── package.json
├── App.js
└── index.js

연동

게시글을 가져오는 액션으로 시작해보자. 예시라 모든 코드를 해당 폴더의 index.js에 넣었다. 분리는 편하신대로 하면 되겠다.

빠르게 훑는 generator

  • iterable (돌리고 돌릴 수 있다)
  • 비동기든 동기든 간에 yield 구문으로 순차적 처리가 가능하다.

type

types/index.js
export const FETCH_BOARDS = "FETCH_BOARDS";
export const FETCH_BOARDS_FULFILLED = "FETCH_BOARDS_FULFILLED";
export const FETCH_BOARDS_REJECTED = "FETCH_BOARDS_REJECTED";

action

actions/index.js
export const fetchBoards = () => ({
type: FETCH_BOARDS,
});

// saga에서 호출하는 액션
export const fetchBoardsFulfilled = (boards) => ({
type: FETCH_BOARDS_FULFILLED,
payload: boards,
});

// saga에서 호출하는 액션
export const fetchBoardsRejected = (error) => ({
type: FETCH_BOARDS_REJECTED,
error,
});

action이 pure object만을 반환하는 것을 보고 있으면 아름답다는 생각이 저절로 든다.

reducer

reducers/index.js
const INITIAL_STATE = {
boards: [],
};

export default (state = INITIAL_STATE, { type, payload, error }) => {
switch (type) {
case FETCH_BOARDS_FULFILLED:
return {
...state,
boards: payload,
};
case FETCH_BOARDS_REJECTED:
return {
...state,
showError: true,
error,
};
default:
return state;
}
};

saga

saga는 action을 listen(watch)한다.

saga/index.js
import { call, spawn, put, takeEvery } from "redux-saga/effects";
import * as actions from "../actions";
import axios from "axios";

function* fetchBoardsSaga() {
// try catch finally 구문으로 오류 제어가 가능하다.
try {
// 이부분을 call 메소드를 이용해 테스트가 쉽게 바꿀 수 있다.
// (yeild를 사용하기 때문에 next 명령어로 반복 가능하므로)
// const { data } = yield call([axios, 'get'], '/boards')
const { data } = yield axios.get("/boards");
yield put(actions.fetchBoardsFulfilled(data));
} catch (error) {
yield put(actions.fetchBoardsRejected(error.response));
}
}

function* watchBoard() {
// type의 action이 실행되면 fetchBoardsSaga도 항상(Every) 실행한다
yield takeEvery(FETCH_BOARDS, fetchBoardsSaga);
}

// 모든 listener(watcher)를 하나로 묶어준다.
// rootReducer에 묶어주는 그것과 같다고 보면 된다.
export default function* root() {
yield spawn(watchBoard);
}

왜 watcher들을 spawn으로 묶어야하는지는 이슈에 나와있다. (여기엔 자동으로 saga가 재시작되는 패턴도 있는데, 아직 활용해본 적은 없다)

action => saga => action => reducer 로 연결되는 saga가 완성되었다.

store

스토어 담는건 각자의 취향이니 어떻게 연결하는지만 보면 된다.

store/index.js
import createSagaMiddleware, { END } from "redux-saga";
const saga = createSagaMiddleware();

export default function configureStore(initialState) {
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(...middlewares),
inDevelopment && window.devToolsExtension
? window.devToolsExtension()
: (f) => f,
),
);

store.runSaga = saga.run;
store.close = () => store.dispatch(END);

return store;
}
index.js
const store = configureStore();
store.runSaga(rootSaga);

메소드

all

yield 구문으로 기다리는 건 순차적으로 실행되기 때문에, 동시에 실행되고 전부 resolve되는 패턴이 필요하다면 all 메소드를 사용하면 된다. (Promise.all을 생각하면 쉽다)

export function* testSaga() {
// 기존 포스팅들에는 이렇게 사용하라는 구문이 많은데
// deprecated 경고를 뱉는다
const [response1, response2] = yield [asyncTask1(), asyncTask2()];

// 아래와 같이 사용하자
const [response3, response4] = yield all([asyncTask1(), asyncTask2()]);
}

call

Function.prototype.call() 함수와 같다.

takeEvery

모든 액션시마다 실행 된다. GET 메소드에 사용하자.

takeLatest

액션 호출시에 같은 액션이 실행 중이면 그 액션은 파기되고 마지막 호출만 실행된다. POST, PUT, DELETE 같은 리소스 변경 메소드에 사용하자.

put

액션을 호출한다. dispatch라고 보면 된다.

다시보기

비유를 통한 설명이 좋으면 이렇게 이해해도 된다. 회사에서 주로 푸드플라이로 서브웨이를 주문하는데 거기에 빗대어 보았다.

1. Board를 가져오는 타입 만들기

  • 서브웨이를 푸드플라이로 시킨다고 한다면
  • const 푸드플라이_서브웨이 = '푸드플라이_서브웨이' 처럼 컴퓨터가 알 수 있게 변수로 행동(액션)의 종류를 설정해주는 것
  • const 푸드플라이_서브웨이_주문성공 = '푸드플라이_서브웨이_주문성공' 처럼 주문이 완료됬을 때의 행동명도 만들 수 있다.

2. fetchBoards 액션 만들기

  • 푸드플라이에서 주문서를 넣는 행동과 같다.
  • 액션은 타입과 결과(payload)를 return하는 함수이다.
  • 굳이 payload를 안 써도 되는데, 그게 예쁘다.

3. fetchBoards 사가 만들기

  • 푸드플라이 서버에서 주문서를 처리하는 것
  • 서버에서 디비 값을 바꾸고 서브웨이로 주문을 밀어넣어주는 것
  • 마지막에 주문이 들어갔다는 다른 액션을 put으로 호출한다.

4. fetchBoards의 액션과 사가를 연결하기

  • 주문서를 넣으면 푸드플라이의 주문서 로직을 실행해야된다는 걸 컴퓨터에게 알려주는 부분
  • takeEvery로 감싸면 매번 주문서가 들어올 때마다 로직이 실행된다는 것
  • takeLatest로 감싸면 매번 주문서가 들어올 때마다 마지막에 들어온 것만 실행하는 것
  • take는 무한루프가 안 예쁘다.

5. fetchBoards 액션이 완료될 경우 발생하는 fetchBoardsFulfilled 액션의 리듀서를 만들기

  • 서브웨이에서 주문서를 받아서 서브웨이 멜트 만드는 준비를 하는 부분

6. 리듀서에서 초기 상태 값을 업데이트 해주기

  • 초기상태값은 서브웨이 멜트에 터키가 최소 2장, 햄 최소 2장 들어간다 같은 것
  • 업데이트는 서브웨이 멜트를 진짜 만드는 행동

7. 컴포넌트(클래스)를 리덕스 스토어와 연결

8. Boards Prop과 fetchBoards Action을 가져오기 (map...toProps 아실 거에요)

9. componentDidMount, onClick에서 fetchBoards 실행

10. Boards.map 등 DOM에 바인딩

여담

  • Saga API Docs엔 action을 기다리는 take 기능 등 많은 고급 기능이 있다.
  • supervisor 패턴으로 구성한 사람도 보이는데 아직 필요성을 잘 못 느끼겠다.
  • 처음부터 mobx로 시작했어도 쉬웠을텐데
  • saga에서 쓰는 개념들은 rx의 스트림과 비슷하게 느껴졌다.
  • 2020년에는 Redux hooks를 사용하면 된다.

잘 이해가 안 된다면 mobx를 이해하는 게 정신건강에 좋습니다.

React-Router Code Splitting - 가장 쉬운 방법

· 약 2분

create-react-app으로 생성한 리액트 앱에 시작시 가져오는 컴포넌트 빼고는 비동기로 불러와야 메인이 가벼워진다.

검색해보면 AsyncComponent를 만들라는 게 보이는데, 더 쉬운 방법이 있다.

React-router 의 Code Splitting탭 에서 찾아볼 수 있는데, 아주 간결하게 컴포넌트를 불러 온다.

import React, { Component } from "react";
import Loadable from "react-loadable";
import Loading from "./Loading";

const LoadableComponent = Loadable({
loader: () => import("./Dashboard"),
/* 컴포넌트 로딩시 보여지는 로딩 컴포넌트 */
loading: Loading,
});

export default class LoadableDashboard extends Component {
render() {
return <LoadableComponent />;
}
}

너무 너무 쉽다. 진작에 이 정도 레벨의 추상화가 있었어야했다.

옵션

자세한 옵션 delay, timeout 등은 여기서 확인할 수 있다.