patch-package

서드파티 라이브러리를 커스텀한 상태가 배포상태에서도 지속되도록 관리해주는 패키지.

즉, node_modules에 수정한 사항이 git으로 관리되고 어떠한 실행 환경에서도 적용되도록 한다.

patch-package를 사용할 시 매번 버전이 달라질 때마다 patch 파일의 버전을 인스톨 되어 있는 버전과 맞춰줘야 하는 단점이 있다.

 

1. 설치

npm install —save-dev patch-package

 

2. 설정

// package.json

"scripts": {
	"postinstall": "patch-package", // 해당 스크립트 추가
},

 

 

3. 변경하고자 하는 패키지 코드 수정

패키지 코드를 원하는대로 수정한다.

 

 

4. patch 적용

npx patch-package [변경한 패키지 이름]

 

5. “patches”라는 폴더가 생기고, 폴더 안에 변경한 패치 내용이 저장되어 있음을 확인

 

 

6. node_moduels 을 지웠다가 npm install 해보면 자동으로 패치내용들이 적용되어 있음

 

주의) 수정한 패키지의 patches 버전과 package.json의 패키지 버전이 다르면 에러를 유발하니 버전 체크하는 것이 중요!

반응형

서론

우선 리액트 공식 문서에서 리액트를 '사용자 인터페이스를 구축하기 위한 선언적이고 효율적이며 유연한 JavaScript 라이브러리'로 소개하고 있다. SPA(Single Page Application)형태로 하나의 페이지에서 보여지는 컴포넌트 조합을 동적으로 바꿔가며 화면을 표현하는 형태다. Virtual DOM이라는 개념을 사용해 이러한 SPA를 구현하고 있다. 장, 단점이 존재하지만 자세한 내용은 나중에 다루도록 하겠다.

 

React 특징

1. Component 구조

2. JSX

3. Data Flow

4. Virtual DOM

 

 

1. Component

"컴포넌트"라고 불리는 작고 고립된 부품을 이용해 복잡한 UI를 구성하도록 해주는 것이 리액트이다. 컴포넌트에는 몇 가지 종류가 있다. 크게 2가지로 나누자면 '함수형 컴포넌트', '클래스형 컴포넌트'가 있다. 우선 클래스형 컴포넌트는 다음과 같은 구조를 가지고 있다. 아래에 간단한 예시를 작성해봤다.

class MyClassComponent extends React.Component {
    render() {
    	return(
            <div className="my-component">
            	<h1>Hello World!</h1>
                <ul>
                    <li>React!</li>
                    <li>React Native</li>
                </ul>
            </div>
        );
    }
}

이러한 컴포넌트는 재사용 가능하고 여러 컴포넌트들을 조합해 다양한 형태의 UI를 쉽게 만들 수 있다. 불필요한 반복적 코드를 줄이고 빠르게 UI를 구성할 수 있어 개발 생산성에 있어 장점을 가진다. 또한 작은 형태의 컴포넌트는 테스트하기 용이해 코드를 유지보수하기에도 도움이 된다. 아래에 여러 작은 컴포넌트를 조합한 예시 코드가 있다.

class App extends Component {
  render() {
    return (
      <Layout>
        <Header />
        <Navigation />
        <Content>
          <Sidebar></Sidebar>
          <Router />
        </Content>
        <Footer></Footer>
      </Layout>
    );
  }
}

 

 

2. JSX

리액트에서는 HTML의 태그 문법과 비슷한 형태로 ui 화면을 표현하고 있다. HTML과 비슷하지만 HTML과는 다르다. 리액트에서는 JSX라 하는 JavaScript를 확장한 문법을 사용한다. 간단하게 JavaScript이지만 화면을 표시하기 위해서 조금 더 확장된 개념이라고 보면 된다. 

JSX란?
Javascript를 확장한 문법입니다.

React에서는 이벤트가 처리되는 방식, 시간에 따라 state가 변하는 방식, 화면에 표시하기 위해 데이터가 준비되는 방식 등 렌더링 로직이 본질적으로 다른 UI 로직과 연결된다는 사실을 받아들입니다.

React는 별도의 파일에 마크업과 로직을 넣어 기술을 인위적으로 분리하는 대신, 둘 다 포함하는 “컴포넌트”라고 부르는 느슨하게 연결된 유닛으로 관심사를 분리합니다. 이후 섹션에서 다시 컴포넌트로 돌아오겠지만, JS에 마크업을 넣는 게 익숙해지지 않는다면 이 이야기가 확신을 줄 것입니다.

React는 JSX 사용이 필수가 아니지만, 대부분의 사람은 JavaScript 코드 안에서 UI 관련 작업을 할 때 시각적으로 더 도움이 된다고 생각합니다. 또한 React가 더욱 도움이 되는 에러 및 경고 메시지를 표시할 수 있게 해줍니다.

출처: React 공식문서

 

 

3. Data Flow - 단방향 데이터 바인딩

단방향 데이터 바인딩은 간단히 말해 데이터의 흐름이 한 방향으로만 이뤄진다. 부모 요소에서 자식 요소로 전달이 되고 적절한 Event를 통해 데이터를 갱신하게 된다. 양방향 바인딩은 앱의 규모가 커질수록 추적하기 어려워지고 그에 따라 복잡해지게 된다. 이런 단점을 보완해 데이터 흐름을 보다 예측 가능하도록 하고자 리액트에서 단방향 형태로 바인딩을 하도록 했다고 한다. 단방향, 양방향 각각 장, 단점이 존재하기에 사용하는 프레임워크나 라이브러리에 따라 사용되는 데이터 바인딩 형태도 다 다르다. 조금 더 깊은 내용은 검색을 하면 많이 나오니 참고하면 될 것 같다.

 

 

4. Virtual DOM

브라우저는 화면을 그리기 위해서 DOM(Document Object Model)이라는 개념을 사용한다. DOM은 HTML 파일 내용을 토대로 만들어지는데, JavaScript와 같은 스크립팅 언어로 수정할 수 있도록 만들어진, 웹 페이지의 객체 지향 표현이다. DOM은 브라우저가 화면을 그리기 위해서 필요한 정보가 트리 형태로 저장된 데이터다. DOM에 변화가 생기면 렌더 트리를 재생성하고 레이아웃을 만들고 다시 보여주는 과정이 반복된다. DOM 트리가 재생성되는 과정에서 모든 요소들이 다시 계산되고 많은 연산을 반복하게 되면서 비효율적이게 된다. React에서는 이런 불필요하고 비효율적인 방식을 개선하고자 Virtual DOM이라는 개념을 도입했다.

React에서 Virtual DOM은 UI의 이상적인 또는 가상의 표현을 메모리에 저장하고 ReactDOM과 같은 라이브러리에 의해 실제 DOM과 동기화하는 프로그래밍적 개념이다. 뷰에서 변화가 생기면 실제 DOM에 적용되기 전 가상 DOM에서 먼저 적용이 되고 비교를 통해 최종적 결과가 실제 DOM으로 전달이 된다. 이런 DOM 관리 과정을 리액트 내에서 자동화, 추상화를 통해 불필요한 연산 비용을 줄이고 성능을 높이도록 해준다.

 

참고자료: 리액트 공식문서 - Virtual DOM

 

반응형

리액트에서 테스트를 위해서 많이 사용하는 도구 중 하나인 React Testing Library에 대한 포스트입니다.

테스트에 사용되는 라이브러리들이 많으니 각자 본인에게 맞는 것을 활용해 공부하며 적용해 보는 것이 좋을 것 같습니다.

아래에 리액트 공식 사이트에서 사용되는 테스트 유틸 및 라이브러리 예시를 확인해보세요.

  • Enzyme: React를 위한 JavaScript 테스트 유틸.
  • Jest: React를 포함한 JavaScript 테스트 프레임워크.
  • react-testing-library: 가벼운 React DOM 테스트 유틸.
  • React-unit: React를 위한 가벼운 단위테스트 라이브러리.
  • Skin-deep: 얕은 렌더링을 지원하는 React 테스트 유틸.
  • Unexpected-react: React 컴포넌트와 이벤트를 발생시켜주는 플러그인.

설치

yarn add @testing-library/react @testing-library/jest-dom

npm install --save react-testing-library @testing-library/jest-dom

npm 또는 yarn을 이용해 react-testing-library를 설치한다. react-testing-library@testing-library/react 로 변경되었으니 주의!

jest-dom@testing-library/jest-dom 으로 변경됐다고 한다.

react-testing-library는 '사용자 관점'에서 테스트를 진행한다. 보통 Enzyme를 이용한 테스트 방식은 상태값, 상태 변수에 대해 테스트를 한다. 하지만 전자는 상태 관리는 컴포넌트의 구현 세부사항일 뿐이다. 즉, 상태 변수가 언제든 다른 컴포넌트로 옮겨지거나, react에서 vue.js로 바뀌더라도 테스트에는 문제가 없어야한다.

다양한 쿼리

  • getBy* 쿼리 (ex. getByTestId, getByText, getByRole): 이 함수들은 동기적(synchronous)이며 그 요소가 현재 DOM 안에 있는지 확인한다. 즉, 현재상태만 확인한다. 그렇지 않으면 에러를 발생시킨다.
  • findBy* 쿼리 (ex. findByText): 이 함수들은 비동기적(asynchronous)이다. 그 요소를 찾을 때까지 일정 시간(기본 5초)을 기다린다. 만약 그 시간이 지난 후에도 요소를 찾을 수 없으면 에러를 발생시킨다.
  • queryBy* 쿼리: 이 함수들은 getBy* 처럼 동기적이다. 하지만 요소를 찾을 수 없어도 에러를 발생시키지 않는다. 단지 null 값을 리턴한다.

테스트 디버깅하기

render(<SomeComponent />);
screen.debug(); //원하는 실행 지점에서 DOM트리를 console에 출력해, 디버깅할 수 있다.

screen.debug() 는 브라우저의 개발 도구처럼 편리하고 상호작용적이진 않지만, 테스트 환경에서 어떤 일이 일어나고 있는지 확실히 알 수 있도록 도와준다.

const link = screen.getByRole('link', { name: /how it works/i });
screen.debug(link);

위와 같이 debug 함수에 파라미터를 전달하면 해당 요소만을 콘솔에 출력해준다.

Mocking

  • jest.fn: Mock a function
  • jest.mock: Mock a module → 자동적으로 모듈의 모든 함수를 mocking 해준다.
  • jest.spyOn: Spy or mock a function → 마찬가지로 모든 함수를 mocking 해주면서, 원래의 함수를 다시 복원할 수도 있다.

가장 기본적인 사용 방식은 함수를 mock 함수로 재할당하는 것이다. 재할당 된 함수가 쓰이는 어디서든지 mock 함수가 원래의 함수 대신 호출 될 것이다.

예시
  1. jest.fn() mocking
// app.js
import * as math from './math.js';

export const doAdd      = (a, b) => math.add(a, b);
export const doSubtract = (a, b) => math.subtract(a, b);
export const doMultiply = (a, b) => math.multiply(a, b);
export const doDivide   = (a, b) => math.divide(a, b);
// math.js
export const add      = (a, b) => a + b;
export const subtract = (a, b) => b - a;
export const multiply = (a, b) => a * b;
export const divide   = (a, b) => b / a;
// app.test.js

import * as app from "./app";
import * as math from "./math";

math.add = jest.fn();
math.subtract = jest.fn();

test("calls math.add", () => {
  app.doAdd(1, 2);
  expect(math.add).toHaveBeenCalledWith(1, 2);
});

test("calls math.subtract", () => {
  app.doSubtract(1, 2);
  expect(math.subtract).toHaveBeenCalledWith(1, 2);
});

  1. jest.mock() mocking
jest.mock('./math.js');

위의 코드로 mocking하는 것은 본질적으로 아래 코드처럼 하는거랑 같다.

export const add      = jest.fn();
export const subtract = jest.fn();
export const multiply = jest.fn();
export const divide   = jest.fn();
// app.test.js

import * as app from "./app";
import * as math from "./math";

// Set all module functions to jest.fn
jest.mock("./math.js");

test("calls math.add", () => {
  app.doAdd(1, 2);
  expect(math.add).toHaveBeenCalledWith(1, 2);
});

test("calls math.subtract", () => {
  app.doSubtract(1, 2);
  expect(math.subtract).toHaveBeenCalledWith(1, 2);
});
반응형

참고로 이 글에서 말하는 테스트는 React환경에서 사용하기 위한 방법을 공부한 것을 정리했다.

우선, Jest를 설치한다.

npm install --save-dev jest

yarn add --dev jest

 

다음으로 간단한 예시로 sum.js 파일을 하나 만들고, 다음과 같이 작성한다.

function sum(a, b) {
  return a + b;
}

 

이제 위의 코드를 테스트하기 위한 sum.test.js 테스트 파일을 하나 생성한다. 

const { sum, sumOf } = require("./sum");

//test, it --> 새로운 테스트 작성
test("adds 1 + 2 to equal 3", () => {
	expect(sum(1, 2)).toBe(3);
});

 

앞서 만든 sum함수를 import해온 다음 테스트 구문에 활용해 제대로 원하는 결과가 테스트되는지 확인하는 코드이다.

이제 테스트 코드를 실행해볼 차례인데, 다음과 같이 pakage.json파일에 추가해준다.

"scripts": {
	"test": "jest"
}

 

아래 명령어를 통해 테스트를 실행하도록 한다. 

npm run test

 

테스트 코드가 제대로 통과가 된다면, 다음과 같은 결과가 나타날 것이다. 참고로 아래 결과는 VSCode를 사용했을 때 나타난 결과이다.

PASS  ./sum.test.js
✓ adds 1 + 2 to equal 3 (5ms)

 

추가적으로 describe를 사용을 해보기 위해 sum.js파일에 새로운 함수를 추가해주자. 여기서 describe 메서드는 여러 테스트 케이스를 묶어주는 기능을 한다.

// sum.js

function sum(a, b) {
  return a + b;
}

// 배열의 합
function sumOf(numbers) {
  return numbers.reduce((acc, current) => acc + current, 0);
}

module.exports = { sum, sumOf };

 

그리고 테스트 코드 역시 수정해주고, 실행을 하게 되면 테스트 결과가 나타난다.

// sum.test.js
const { sum, sumOf } = require("./sum");

describe("sum", () => {
  it("calculates 1 + 2", () => {
    expect(sum(1, 2)).toBe(3);
  });

  it("calculates all numbers", () => {
    const array = [1, 2, 3, 4, 5];
    expect(sumOf(array)).toBe(15);
  });
});

test 대신 it을 사용해도 같은 기능이다. 다만 it을 사용하게 되면, 테스트 케이스 설명을 영어로 작성할 경우 "말이 되게" 매끄럽게 작성 가능해져 사용한다고 한다.

반응형

Boolean

let isDone: boolean = false;

 

Number

타입스크립트에서 숫자는 부동 소수점 값 또는 BigInteger 값이다. 16진수, 10진수, 8진수, 2진수 타입도 지원한다.

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: bigint = 100n;

 

String

자바스크립트와 똑같이 double quotes(""), single quotes('')로 문자열을 감싼다.

let color: string = "blue";
color = 'red';

마찬가지로 backtick (`${ expr }`) 형태로 사용하면 된다.

let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}.

I'll be ${age + 1} years old next month.`;



Array

let list: number[] = [1, 2, 3];

let list: Array<number> = [1, 2, 3]; //제네릭 타입방식

 

유니언 타입(다중 타입)의 ‘문자열과 숫자를 동시에 가지는 배열’도 선언할 수 있다.

// Union 이용한 배열
let array: (string | number)[] = ['Apple', 1, 2, 'Banana', 'Mango', 3];

let array: Array<string | number> = ['Apple', 1, 2, 'Banana', 'Mango', 3];

 

배열이 가지는 항목의 값을 단언할 수 없다면 any를 사용할 수 있다.

let array: any[] = [0, 1, {}, [], 'str', false];

 

인터페이스(Interface)나 커스텀 타입(Type)을 사용할 수도 있다.

interface IUser {
  name: string,
  age: number,
  isValid: boolean
}

let userArr: IUser[] = [
  {
    name: 'Jiho',
    age: 25,
    isValid: true
  },
  {
    name: 'Juliet',
    age: 42,
    isValid: false
  },
  {
    name: 'Evan',
    age: 52,
    isValid: true
  }
];

 

readonly 키워드나 ReadonlyArray 타입을 사용해 읽기 전용 배열을 생성할 수도 있다.

let arrA: readonly number[] = [1, 2, 3, 4];
let arrB: ReadonlyArray<number> = [0, 5, 5, 2];

arrA[0] = 123; // Error - TS2542: Index signature in type 'readonly number[]' only permits reading.
arrA.push(123); // Error - TS2339: Property 'push' does not exist on type 'readonly number[]'.

arrB[0] = 123; // Error - TS2542: Index signature in type 'readonly number[]' only permits reading.
arrB.push(123); // Error - TS2339: Property 'push' does not exist on type 'readonly number[]'.

 

Tuple

배열 요소의 수가 고정된 형태의 자료형이다. 각각의 요소별로 타입을 지정해주면 된다.

// Declare a tuple type
let x: [string, number];

// Initialize it
x = ["hello", 10]; // OK

// Initialize it incorrectly
x = [10, "hello"]; // Error

 

Enum

특정 값들의 집합을 의미하는 자료형이다. 타입스크립트에서는 문자형 이넘과 숫자형 이넘을 제공 한다.

enum Color {
  Red, //0
  Green, //1
  Blue, //2
}
let c: Color = Color.Green;

 

기본적으로 enum은 인덱스 번호가 0번부터 시작하지만, 따로 지정해 변경해줄 수 있다. 또는 모든 값에 수동으로 설정해주는 것도 가능하다. 이때 번호 대신 문자열도 지정 가능하지만, 모든 값에 설정해줘야만 한다는 것을 주의해야 한다.

enum Color {
  Red = 1,
  Green, //2
  Blue, //3
}
let c: Color = Color.Green;

enum Color {
  Red = 1,
  Green = 2,
  Blue = 4,
}
let c: Color = Color.Green;

enum Color {
  Red = "red",
  Green = "green",
  Blue = "blue",
}

 

리버스 매핑(Reverse Mapping) - 숫자형 이넘에만 존재하는 특징이다. enum의 키(key)로 값(value)을 얻을 수 있고 값(value)으로 키(key)를 얻을 수도 있다. enum 요소에 해당하는 번호를 통해 값을 얻을 수 있다.

enum Color {
  Red = 1,
  Green,
  Blue,
}
let colorName: string = Color[2];

// Displays 'Green'
console.log(colorName);

 

 

Void

일반적으로 값을 반환하지 않는 함수에서 사용한다. : void 위치는 함수가 반환 타입을 명시하는 곳이다.

값을 반환하지 않는 함수는 실제로는 undefined를 반환한다.

function hello(msg: string): void {
  console.log(`Hello ${msg}`);
}
const hi: void = hello('world'); // Hello world
console.log(hi); // undefined

 

Object

object는 원시 타입이 아닌 타입을 나타낸다. number, string, boolean, bigint, symbol, null, 또는 undefined 가 아닌 나머지를 의미한다.

declare function create(o: object | null): void;

create({ prop: 0 }); // 성공
create(null); // 성공

create(42); // 오류
create("string"); // 오류
create(false); // 오류
create(undefined); // 오류

 

 

타입 단언(Type assertions)

컴파일러에게 개발자가 명확하게 타입에 대해서 알려주는 방법이다. 예를 들어 타입스크립트의 타입인 string가 아닌 조금 더 명확한 "some_type"이라고 하는 명확한 타입이라고 알려주는 경우다.

 

다른 언어의 타입 변환(형 변환)과 유사하지만, 다른 특별한 검사를 하거나 데이터를 재구성하지는 않는다. 이는 런타임에 영향을 미치지 않으며, 온전히 컴파일러만 이를 사용한다. 타입 스크립트는 개발자가 필요한 어떤 특정 검사를 수행했다고 인지한다.

 

타입 단언의 사용법은 2가지 경우가 있다.

 

- Angle Braket 형태

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

 

- as 형태

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;

위 두 예제는 동일하며 어떤 것을 사용할지는 주로 선호에 따른 선택이다. 하지만 TypeScript를 JSX와 함께 사용할 때는, as-스타일의 단언만 허용된다.

 

 

 

Union

2개 이상의 타입을 지정해주고자 할 때 사용하며, OR연산자 | (vertical bar)를 를 사용해 타입들을 지정한다. 유니언 타입은 여러 타입 중 하나가 될 수 있는 값을 의미한다.

let union: (string | number);
union = 'Hello type!';
union = 123;
반응형

순서

  1. 리덕스 리듀서 모듈 생성(Ducks Pattern) - 한 파일에 액션, 액션 생성 함수, 리듀서를 작성하는 방식
  2. 각 리듀서 모듈에서 액션타입(TS에서의 타입이 아님), 액션 생성 함수, 액션 객체 타입 선언(TS에서의 타입), 초기값, 기본값 타입들, 리듀서를 작성한다.
  3. '루트 리듀서'역할을 하는 index.ts를 만들어, 리덕스의 combineReducers를 이용해 만든 리듀서 모듈들을 하나로 통합해 export 한다.
  4. 프로젝트 전체에 리듀서를 적용하고, 스토어에 접근 가능하도록 앱의 최상위 파일에서 store를 생성해 Provider를 이용해 적용시킨다.
  5. 필요한 컴포넌트들을 작성한다. 여기서, useSelector , useDispatch 를 이용해 각각 조금 더 쉽게 리듀서를 연결하고 스토어에 접근해 값을 사용하고, 액션을 발생시켜 상태를 변화시킨다.

 

리듀서 모듈 생성

리덕스 사용에 필요한 액션, 액션 생성함수, 리듀서를 한 파일에 작성한다. 이러한 방식을 'Ducks 패턴'이라 한다.

우선, 필요한 액션 타입을 상수로 설정한다.

const ADD_TODO = "todo/ADD_TODO" as const;
const REMOVE_TODO = "todo/REMOVE_TODO" as const;
const TOGGLE_TODO = "todo/TOGGLE_TODO" as const;

 

위와 같이 타입스크립트 문법인 Type Assertions(타입 단언)을 사용해 액션 타입에 대한 타입 추론의 범위를 줄여 string 타입이 아닌 명확히 액션 타입을 추론하도록 해준다. 액션 타입들을 상수화 시켜두면 추후에 코드 수정에 있어 용이해진다.

 

 

다음으로 액션 생성 함수를 작성한다. 모듈 외부에서 사용할 수 있도록 export 해준다. Context api나 Redux 에서 액션을 dispatch를 이용해 액션을 발생시킬 때 해당 액션 생성 함수들을 사용한다.

// Action Creators
export const addTodo = (text: string) => ({ type: ADD_TODO, payload: text });
export const removeTodo = (id: number) => ({ type: REMOVE_TODO, payload: id });
export const toggleTodo = (id: number) => ({ type: TOGGLE_TODO, payload: id });

 

다음으로 액션 객체에 대한 타입들을 설정해줘야 한다.

// 액션 객체 타입 설정
type TodoAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof removeTodo>
  | ReturnType<typeof toggleTodo>;

 

ReturnType<T> 는 유틸리티 타입의 한 종류로서 전역적으로 사용 가능하다. 기능으로는 함수 T반환 타입으로 구성된 타입을 만들어준다. 아래에 간단한 예시를 살펴보자.

declare function f1(): { a: number, b: string }
type T0 = ReturnType<() => string>;  // string
type T1 = ReturnType<(s: string) => void>;  // void
type T2 = ReturnType<(<T>() => T)>;  // {}
type T3 = ReturnType<(<T extends U, U extends number[]>() => T)>;  // number[]
type T4 = ReturnType<typeof f1>;  // { a: number, b: string }
type T5 = ReturnType<any>;  // any
type T6 = ReturnType<never>;  // any
type T7 = ReturnType<string>;  // 오류
type T8 = ReturnType<Function>;  // 오류

 

ReturnType으로 인한 위의 액션 객체의 타입은 다음과 같이 될 것이다. 앞서 Type Assertions으로 const로 지정을 해주지 않았다면 제대로 동작하지 않을 수도 있다.

 

 

다음으로 리듀서 함수에 전달 할 state에 대해 필요한 타입과 초기값들을 지정해준다.

export type Todo = {
  id: number;
  text: string;
  isToggle: boolean;
};

export type Todos = Todo[];

export const initialState: Todos = [
  {
    id: 0,
    text: "타입스크립트 투두리스트",
    isToggle: false
  },
  {
    id: 1,
    text: "타입스크립트 리덕스",
    isToggle: false
  }
];

 

마지막으로 앞서 작성한 state와 action 객체에 대한 타입을 이용해 리듀서 함수를 작성한다.

// Reducer
export default function todoReducer(state: Todos = initialState, action: TodoAction): Todos {
  switch (action.type) {
    case ADD_TODO:
      const id = Math.max(...state.map((todo) => todo.id)) + 1; // 현재 있는 리스트 중 가장 큰 id값에 +1 한다.

      return state.concat({
        id,
        text: action.payload,
        isToggle: false
      });

    case REMOVE_TODO:
      return state.filter((todo) => todo.id !== action.payload);

    case TOGGLE_TODO:
      return state.map((todo) =>
        todo.id === action.payload
          ? { ...todo, isToggle: !todo.isToggle }
          : todo
      );

    default:
      return state;
  }
}

 

최종적인 리듀서 모듈의 코드는 다음과 같다.

// src/modules/todo.ts


// Action Type
const ADD_TODO = "todo/ADD_TODO" as const; // Type Assertions(현재 나타내는 타입보다 더 구체적인 타입을 나타내려 할 때 이용)
const REMOVE_TODO = "todo/REMOVE_TODO" as const;
const TOGGLE_TODO = "todo/TOGGLE_TODO" as const;

// Action Creators
export const addTodo = (text: string) => ({ type: ADD_TODO, payload: text });
export const removeTodo = (id: number) => ({ type: REMOVE_TODO, payload: id });
export const toggleTodo = (id: number) => ({ type: TOGGLE_TODO, payload: id });

// 액션 객체 타입 설정
// ReturnType --> 타입스크립트의 특정함수의 반환 타입을 추출해내는 제네릭 타입으로
// 이를 통해 interface 중복작성을 피할 수 있다.
type TodoAction =
  | ReturnType<typeof addTodo>
  | ReturnType<typeof removeTodo>
  | ReturnType<typeof toggleTodo>;

//---- 리듀서에 전달 할 state에 대한 처리 ----
export type Todo = {
  id: number;
  text: string;
  isToggle: boolean;
};

export type Todos = Todo[];

export const initialState: Todos = [
  {
    id: 0,
    text: "타입스크립트 투두리스트",
    isToggle: false
  },
  {
    id: 1,
    text: "타입스크립트 리덕스",
    isToggle: false
  }
];

// ------------------------------------

// Reducer
export default function todoReducer(
  state: Todos = initialState,
  action: TodoAction
): Todos {
  switch (action.type) {
    case ADD_TODO:
      const id = Math.max(...state.map((todo) => todo.id)) + 1;
      return state.concat({
        id,
        text: action.payload,
        isToggle: false
      });

    case REMOVE_TODO:
      return state.filter((todo) => todo.id !== action.payload);

    case TOGGLE_TODO:
      return state.map((todo) =>
        todo.id === action.payload
          ? { ...todo, isToggle: !todo.isToggle }
          : todo
      );

    default:
      return state;
  }
}

 

 

Root reducer 만들기

리덕스의 combineReducers를 이용해 만든 리듀서 모듈들을 하나로 통합해 만든 루트 리듀서 파일을 export 한다.

// src/modules/index.ts

import { combineReducers } from "redux";
import todoReducer from "./todo";

const rootReducer = combineReducers({
  // 여러 리듀서 모듈들을 하나로 병합한다.
  todoReducer
});

export default rootReducer;
export type RootState = ReturnType<typeof rootReducer>;

 

 

최상위 컴포넌트 Provider 감싸기

최상위 컴포넌트 파일인  루트의 index.tsx 파일에서 <Provider> 컴포넌트로 하위 모든 컴포넌트에서 store에 접근이 가능하게 해 준다. 

// ./index.tsx

import { render } from "react-dom";
import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";

import rootReducer from "./modules";
import App from "./App";

const store = createStore(rootReducer); // store 생성

const rootElement = document.getElementById("root");
render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

 

필요한 컴포넌트들 작성

Redux를 사용하기 위한 준비를 다 마쳤다. 이제 컴포넌트들을 작성하고 Redux 기능들을 활용하면 된다.

src 폴더 안에 components폴더를 만들고, TodoApp.tsx 와 TodoAppContianer.tsx파일을 만든다. 각각의 코드는 다음과 같다.

 

// /components/TodoApp.tsx

import React, { useState } from "react";
import { Todos, Todo } from "../modules/todo";
import { useDispatch } from "react-redux";
import { addTodo, toggleTodo, removeTodo } from "../modules/todo";

type Props = {
  todos: Todos;
};

const TodoApp = ({ todos }: Props) => {
  // 디스패치 함수에 파라미터로 액션객체를 넣어주면 스토어로 전달하여 액션을 발생시키고,
  // 리듀서 모듈에 이 액션이 있다면 새로운 상태로 바뀌게 되는것
  const dispatch = useDispatch();
  const [input, setInput] = useState("");

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    dispatch(addTodo(input));
    setInput("");
  };

  const handleClick = (id: number) => {
    dispatch(toggleTodo(id));
  };

  const handleRemove = (id: number) => {
    dispatch(removeTodo(id));
  };

  const done = {
    textDecoration: "line-through"
  };

  return (
    <>
      <div>
        <form onSubmit={(e) => handleSubmit(e)}>
          <input value={input} onChange={(e) => setInput(e.target.value)} />
          <button type="submit">등록</button>
        </form>
      </div>
      <div>
        {todos.map((todo) => {
          const { id, text, isToggle } = todo;
          return (
            <div
              key={id}
              onClick={() => handleClick(id)}
              style={isToggle ? done : undefined}
            >
              {text}
              <button onClick={() => handleRemove(id)}>삭제</button>
            </div>
          );
        })}
      </div>
    </>
  );
};

export default TodoApp;

 

위의 코드에서 dispatch 함수의 인자로 '액션 생성함수(action creator)'를 넣어줬다. 즉, modules 폴더에서 작성한 리듀서 파일 안의 액션 생성 함수들의 결과로 '액션 객체' 값을 리턴 받아 dispatch 하게 된다. 그런 다음 reducer에서 액션 타입에 맞는 로직을 수행하게 되고 store의 상태 값을 변화시키게 된다.

 

// /components/TodoAppContainer.tsx

import React from "react";
import TodoApp from "./TodoApp";
import { useSelector } from "react-redux";
import { RootState } from "../modules";

const TodoAppContainer = () => {
  // useSelector :: 간단하게 리듀서 모듈과 연결해 데이터를 받아올 수 있다.
  const todos = useSelector((state: RootState) => state.todoReducer);
  return <TodoApp todos={todos} />;
};

export default TodoAppContainer;

useSelector는connect 를 통해 상태 값을 조회하는 것보다 훨씬 간결하게 작성하고 코드 가독성이 상승되는 장점이 있는 함수다.

 

최종적으로 다음과 같이 나타나게된다.

 

반응형

'JavaScript > Redux' 카테고리의 다른 글

[Redux] Redux 개념 익히기  (0) 2021.02.28

Redux는 애플리케이션 상태를 관리하기 위한 오픈 소스 JavaScript 라이브러리다.

 

1. Redux 등장 배경

리덕스가 등장하기 이전 프론트엔드에서 데이터 흐름(형상)을 관리하는 방식은 MVC 패턴이었다. MVC 패턴의 큰 특징 중 하나가 ‘양방향 데이터 흐름’이다. 모델이 변경된다면 뷰 또 변경되고, 사용자에 의해 뷰에서 변경이 일어난다면 모델 또한 변경된다. 이러한 양방향 데이터 흐름은 설계하기 간단하고 코드 작성하기 쉬운 장점이 있다. 하지만 애플케이션 규모가 커진다면 문제가 생긴다. 한 개의 모델이 여러 개의 뷰를 조작하고 한 개의 뷰가 여러 개의 모델을 조작한다면 데이터 흐름을 이해하기 힘들어진다. 버그를 찾기 어려워지고 데이터 흐름을 추적하는 데 많은 시간을 투자해야 하는 단점이 있다.

 

  • Model— 데이터의 형식이나 구조를 관리한다. 모델에 맞지 않는 데이터는 흐름을 제어받을 수 있다.

  • View — 코드가 사용자에게 보여지는 부분을 담당한다. 사용자에게 보여지는 모습과 형태를 관리한다.

  • Controller — 변화하는 데이터를 관리한다. View에서 발생하는 이벤트로 변경되는 데이터나 서버로부터 받은 변경된 데이터를 Model과 View에 업데이트해준다.

출처: http://aalmiray.github.io/griffon-patterns

 

 

2. Flux 등장

페이스북에서 MVC 패턴으로 데이터 흐름을 관리하는 데 많은 어려움을 겪고 있었고 그의 대안으로 Flux라는 새로운 아키텍처 패턴을 개발하였다.

 

 

Flux는 MVC 패턴에서 겪은 복잡한 상황을 개선하는 것이 목적이었고 그 방법으로 ’ 단방향 데이터 흐름’을 적용한 것이다.

 

  • View는 MVC 패턴과 달리 데이터를 변경시키지 않고 Action을 넘겨준다.

  • Action은 반드시 Dispatcher를 지나게 되고 Dispatcher를 통해서 데이터 변경이 일어나고 View는 변경된 데이터를 Store를 통해서 전달받는다.

 

이러한 단방향 데이터 흐름은 기존의 MVC 패턴에 있던 ‘상태의 전이’(뷰와 모델 사이의 데이터 변경이 연결된 수많은 곳으로 따라 변경되는 현상) 현상을 없애주고 ‘예측 가능하다’는 특징을 갖는다.

 

 

3. Redux의 등장

2015년에 Dan Abramov에 의해서 React + Flux의 구조에 ‘Reducer’를 결합한 ‘Redux’가 등장하게 된다.

 

리덕스의 특징 3가지

 

1. Single source of truth 

모든 상태는 하나의 스토어 안에 하나의 객체 트리 구조로 저장

 

2. State is read-only 

상태를 변화시키는 유일한 방법은 무슨 일이 벌어지는 지를 묘사하는 액션 객체를 전달하는 방법뿐이다. 데이터의 변경은 Reducer만 할 수 있다. Reducer 이외의 공간에서는 데이터(상태)는 읽기 모드인 것이다.

 

3. Changes are made with pure functions 

 

변화를 일으키는 함수, 리듀서는 순수한 함수여야 한다.

  • 리듀서 함수는 이전 상태와, 액션 객체를 파라미터로 받습니다.

  • 이전의 상태는 절대로 건드리지 않고, 변화를 일으킨 새로운 상태 객체를 만들어서 반환합니다.

  • 똑같은 파라미터로 호출된 리듀서 함수는 언제나 똑같은 결괏값을 반환해야만 합니다.

만약 매번 같은 입력에 대해 같은 결과를 리턴하지 않는 경우는, 예를 들어 랜덤 숫자를 생성한다던지 혹은, 네트워크에 요청을 한다던지 등 이러한 작업은 결코 순수하지 않은 작업이므로, 리듀서 함수의 바깥에서 처리해줘야 한다. 그런 것을 하기 위해서, 리덕스 미들웨어를 사용할 수 있다.

 

 

리덕스의 구성요소

 

- Store

상태를 관리하고 저장되는 장소이며, 직접적으로 스토어에 접근해서는 안되고 dispatch, subscribe, getState 같은 함수들을 이용해 스토어의 state에 접근한다. 스토어는 현재의 앱 상태와, 리듀서가 들어가 있고, 추가적으로 몇 가지 내장 함수들을 포함하고 있다.

 

  • Dispatch 함수 :  액션을 발생시켜 store에 상태 변화가 필요하다는 것을 알리는 역할을 한다. 호출된 액션은 reducer 함수를 호출시키고, 액션에 맞는 로직과 상태 변화 과정을 거친다.

  • getState 함수 : 현재 애플리케이션의 state 값에 접근한다.

  • Subscribe 함수 : 함수 형태의 값을 파라미터로 받고, action이 dispatch 될 때마다 전달받은 함수를 호출한다.

 

 

 

- Reducer 함수

dispatch에 의해 액션이 전달되면, 액션 객체 값기존 state 값을 참조해 새로운 state 값을 반환해주는 역할을 한다.

const reducer = (currentState, action) => {
	
    // 상태 업데이트 로직
    
    return newState;
}


const reducer = (prevState, action) => {
  switch(action.type) {
    case 'SAVE_MONEY' :
    return {
      ...
    }
    default ...
  }
}

 

- Action 객체

액션 객체는 반드시 type 영역을 필수로 설정해줘야 하며, 그 외의 어떤 데이터는 개발자가 마음대로 넣을 수 있다.

// 액션 객체

{
  type: "ADD_TODO",
  data: {
    id: 0,
    text: "액션 객체 데이터"
  }
}

// 액션 생성함수 --> 인자를 받아와 액션 객체를 반환해주는 함수다.
const changeInput = text => ({ 
  type: "CHANGE_INPUT",
  text
});

 

반응형

'JavaScript > Redux' 카테고리의 다른 글

[Redux] To Do List 예제 - TypeScript, React  (0) 2021.04.26

메모이제이션이란 계산된 값을 자료구조에 저장하고 이후 같은 계산을 반복하지 않고 자료구조에서 꺼내 재사용하는 것을 말한다. 메모이제이션의 대표적인 예로는 동적계획법의 탑다운 방식이 있다.

 

1. 기본 개념

  • useMemo와 useCallback는 메모이제이션 기능을 지원하는 리액트의 내장 훅으로, 퍼포먼스 최적화를 위하여 사용된다.

  • useMemo는 메모이제이션된 값을 반환한다.

  • useCallback은 메모이제이션된 콜백을 반환한다.

리액트는 실제로는 상태가 변경되는 컴포넌트와 그 이하의 모든 자식 컴포넌트가 랜더링의 대상이 된다. 문제는 자식 컴포넌트의 상태가 변경되지 않아도(갱신될 필요가 없어도) 불필요한 랜더링이 일어난다는 것이다. 자바스크립트에서 함수도 참조형 데이터이기 때문에 늘 새로운 값으로 취급되어 동일성을 보장받지 못하므로 리액트에서 매번 새로운 렌더링 대상이 된다.

 

 

2. useMemo

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

“생성(create)” 함수와 그것의 의존성 값의 배열을 전달해야 한다.useMemo는 의존성이 변경되었을 때에만 메모이제이션된 값만 다시 계산할 것이다. 이 최적화는 모든 렌더링 시의 고비용 계산을 방지하게 해 준다.

  • 배열이 없는 경우 매 렌더링 때마다 새 값을 계산하게 된다.

  • useMemo로 전달된 함수는 렌더링 중에 실행된다.

  • 무분별하게 useMemo를 사용하면 오히려 성능이 저하될 수 있다. 따라서 최대한 사용하지 않고 useEffect Hook 등을 활용해 비동기적으로 동작하도록 고안하는 것이 좋다. (리액트 공식 문서에서 추천하는 방식)

 

3. useCallback

const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

콜백과 그것의 의존성 값을 배열로 만들어 인자로 전달하며, 메모이제이션된 콜백함수를 반환한다. 그 메모이제이션된 버전은 콜백의 의존성이 변경되었을 때에만 변경된다. 이것은, 불필요한 렌더링을 방지하기 위해(예로 shouldComponentUpdate를 사용하여) 참조의 동일성에 의존적인 최적화된 자식 컴포넌트에 콜백을 전달할 때 유용하다.

 

  • useCallback(fn, deps) useMemo(() => fn, deps)와 같다고 한다. 

  • useEffect와 마찬가지로 두 번째 인자로 빈 배열([])을 넣으면 어떤 상태 값에도 반응하지 않으며, 두 번째 인자로 아무것도 넣지 않으면 모든 상태 변화에 반응한다.

반응형

+ Recent posts