본문으로 건너뛰기

프로젝트 기본세팅 추가설정

ESlint, prettier

npm init @eslint/config npm i -D prettier eslint-config-prettier

//.eslintrc.js
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['standard-with-typescript', 'plugin:react/recommended'],
overrides: [
{
env: {
node: true,
},
files: ['.eslintrc.{js,cjs}'],
parserOptions: {
sourceType: 'script',
},
},
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['react'],
rules: {
semi: ['error', 'always'], // 세미콜론을 항상 사용하도록 설정
'comma-dangle': ['error', 'always-multiline'], // 마지막 콤마를 항상 허용
},
extends: ['airbnb-base', 'prettier'], //충돌방지
};
//.prettierrc
{
// 쌍따옴표 대신 홑따옴표 사용
"singleQuote": true,
// 모든 구문 끝에 세미콜론 출력
"semi": true,
// 탭 대신 공백으로 들여쓰기
"useTabs": false,
// 들여쓰기 공백 수
"tabWidth": 2,
// 가능하면 후행 쉼표 사용
"trailingComma": "all",
// 줄 바꿈할 길이
"printWidth": 80,
// 객체 괄호에 공백 삽입
"bracketSpacing": true,
// 항상 화살표 함수의 매개 변수를 괄호로 감쌈
"arrowParens": "always",
// OS에 따른 코드라인 끝 처리 방식 사용
"endOfLine": "auto"
}

이 두 개를 추가로 설정하는 과정에서 svg파일이 제대로 동작하지 않았다. 그래서 file-loader를 추가로 설치해봤지만 소용이 없었고, 구글링을 추가로 해본 결과, 타입스크립트에서 제대로 인식하지 못하고 있다는 것을 알았다.

*.d.ts의 역할은 타입스크립트의 컴파일러가 모듈의 타입을 인식할 수 있게 도와주는 것이다.

css-props 인식

아무래도 emotion의 css 방식은 타입스크립트가 인식하는데 어려움이 있는 것 같다.

내가 만든 컴포넌트들에 css를 적용하기 위해선 tsconfig.json의 compilerOptions에 "types": ["@emotion/react/types/css-prop"]를 추가해야 했다.

Redux

가장 많이 사용되는 상태관리 라이브러리중 하나니깐, 알고있는 것만으로도 도움이 되지 않을까 싶다.

설치

redux react-redux @types/react-redux 리액트에서 리덕스를 사용하기 위해선 react-redux도 설치해야 한다.

redux 작동방식

redux는 FLUX패턴을 사용한다. FLUX패턴과 대조되는 패턴으로는 MVC방식이 있다.

MVC는 Model, View, Controller의 약자로, Model로 상태를 정의해 두고, View로 UI기능을 하여 Controller를 통해 Model을 변경하는 구조이다.

하지만 MVC 패턴은 웹이 복잡해지면서 Model, View간의 양방향 접근이 발생하면서 알 수 없는 버그가 발생할 수 있다.(이는 개발자의 잘못이라는 의견도..)

FLUX패턴은 ACTION -> DISPATCHER -> MODEL(store) -> VIEW -> ACTION -> DISPATCHER -> MODEL(store) -> VIEW ... 방식으로 단방향 순환구조를 보여줘 이러한 오류발생을 없앨 수 있다.

redux의 3대 원칙

  1. store는 1개
  2. 상태는 읽기 전용
  3. 리듀서는 순수함수

redux는 크게 중요한 3가지로 action, reducer, store 꼽는다.

  1. ACTION이 발생하면 reducer가 실행된다.
  2. reducer는 dispather부분으로, action이 발생하면 action과 이전 상태 state(store)를 참조해 상태를 갱신시킨다.
  3. store는 상태와 리듀서를 담고 있는 객체이다.

백문이 불여일견. 직접 해보기

// src/modules/counterReducer.ts
const INCREMENT = 'INCREMENT' as const;
const DECREMENT = 'DECREMENT' as const;
const SET_DIFF = 'SET_DIFF' as const;

export const increment = () => ({
type: INCREMENT,
});

export const decrement = () => ({
type: DECREMENT,
});

export const setDiff = (diff: number) => ({
type: SET_DIFF,
diff,
});

type CounterAction =
| ReturnType<typeof increment>
| ReturnType<typeof decrement>
| ReturnType<typeof setDiff>;

export interface CounterState {
count: number;
diff: number;
}

const initialState: CounterState = {
count: 0,
diff: 1,
};

export default function counterReducer(
state: CounterState = initialState,
action: CounterAction
): CounterState {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + state.diff,
};
case DECREMENT:
return {
...state,
count: state.count - state.diff,
};
case SET_DIFF:
return {
...state,
diff: action.diff,
};
default:
return state;
}
}
const INCREMENT = 'INCREMENT' as const;
const DECREMENT = 'DECREMENT' as const;
const SET_DIFF = 'SET_DIFF' as const;

ACTION에 해당하는 부분이다.

export const increment = () => ({
type: INCREMENT,
});

export const decrement = () => ({
type: DECREMENT,
});

export const setDiff = (diff: number) => ({
type: SET_DIFF,
diff,
});

ACTION 생성함수.

action이 발생하면 reducer에 return값을 넘긴다.

type CounterAction =
| ReturnType<typeof increment>
| ReturnType<typeof decrement>
| ReturnType<typeof setDiff>;

export interface CounterState {
count: number;
diff: number;
}

const initialState: CounterState = {
count: 0,
diff: 1,
};

export default function counterReducer(
state: CounterState = initialState,
action: CounterAction
): CounterState {
switch (action.type) {
case INCREMENT:
return {
...state,
count: state.count + state.diff,
};
case DECREMENT:
return {
...state,
count: state.count - state.diff,
};
case SET_DIFF:
return {
...state,
diff: action.diff,
};
default:
return state;
}
}

typescript를 사용하기때문에 타입을 정해줘야 한다.

리듀서는 state(기본상태 포함),action으로 이루어진다.

action의 타입에 따라 행동이 달라진다.

modules 안에 action, store를 지정한다.

리덕스는 말했다시피 하나의 store만을 사용해야 한다.

여러개의 model을 사용해야 한다면, combineReducers를 이용한다.

// src/modules/index.ts
import { combineReducers } from 'redux';
import counterReducer from './counterReducer';

const rootReducer = combineReducers({
counterReducer,
... // 추가적으로 선언할 리듀서들
});

export type RootState = ReturnType<typeof rootReducer>;

export default rootReducer;

::: warning 리듀서 역시 컴포넌트처럼 선언하는 줄 알고

import React from 'react';
import { combineReducers } from 'redux';
import counterReducer from './counterReducer';

export default function rootReducer() {
return combineReducers({ counterReducer });
}

export type RootState = ReturnType<typeof rootReducer>;

방식을 사용했으나, 에러가 떴다. 리듀서는 함수형식이 아니라, 상수 선언형식으로 작성해 사용해야 한다. :::

store를 선언하고, 리액트에게 알려주기 위해 기본 <App/>을 감싸는 <Provider>index.ts에 만든다.

// src/index.tsx
import { Provider } from 'react-redux';
import rootReducer from './modules/index';
import { legacy_createStore as createStore } from 'redux'; //버전 업데이트로 인해 createStore가 없다

const store = createStore(rootReducer);

const root = createRoot(document.getElementById('root') as HTMLElement);
root.render(
<Provider store={store}>
<App />
</Provider>
);

MODEL(store)부분에 해당한다.

createStore를 통해 store를 생성, rootReducer를 기본 상태로 정의한다. 그리고 Provider를 사용해 모든 컴포넌트가 구독할 수 있게 만든다.

// src/components/counter.tsx
import React, { Fragment } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../modules/index';
import { increment, decrement, setDiff } from '../modules/counterReducer';
export default function Counter() {
const dispatch = useDispatch();
const count = useSelector((state: RootState) => state.counterReducer.count);

const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setDiff(Number(e.target.value)));
};
return (
<div>
<h1>{count}</h1>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
<input
type="text"
value={useSelector((state: RootState) => state.counterReducer.diff)}
min={1}
onChange={changeHandler}
/>
</div>
);
}

이 부분으로 VIEW, DISPATCHER을 만든다.

useSelector를 통해 리덕스 store의 상태를 조회한다.

useDispatch를 통해 리덕스의 store의 dispatch를 호출해 상태를 갱신한다.

React.FC<>

리액트+타입스크립트를 사용할때 쓰는 방법이라고 하는데, 요즘은 지양된다고 한다.

<>안에 PROPS타입을 만들고 넣으면 : PROPS와 같은 역할을 한다.

다만 의도치 않은 children등 다른 속성들이 추가되어 타입스크립트의 정의에 위배된다.