Port has been used 에러 해결하기

블로그 설정을 새로 하던 중에 사용중인 port가 있어서 로컬서버가 열리지 않았다. 4000번은 이미 사용중이라는 에러가 발생하였다.

port 에러
1
netstat -ano | findstr 4000

4000번을 쓰고있는 pid 찾기

1
taskkill /f /pid 76860

강제로 종료하기

강제로 종료한 뒤에 확인해보니 정상적으로 종료되었고 로컬에서 정상적으로 서버도 열렸다.

port 강제종료하기

드래그 이벤트 구현하기

드래그

드래그는 클릭 -> 마우스 이동 -> 마우스 떼기로 이루어진다.

마우스 클릭(mousedown)에 이벤트를 등록하고 마우스 떼기(mouseup)에서 이벤트를 지워주면 된다.

또한 브라우저에서 기본으로 제공하는 드래그 이벤트를 없애주면 된다.

mousedown에 mousemove와 mouseup 이벤트를 등록하고 mouse up에서 이를 모두 지워주었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const slider = document.querySelector(".slider");
const thumb = slider.querySelector(".thumb");
const before = slider.querySelector(".balance__goal .complete");
console.log(before);

thumb.onmousedown = function (event) {
event.preventDefault();

let shiftX = event.clientX - thumb.getBoundingClientRect().left;
let End = slider.offsetWidth - thumb.offsetWidth;

document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);

function onMouseMove(event) {
let newComplete =
event.clientX - shiftX - slider.getBoundingClientRect().left;

if (newComplete < 0) {
newComplete = 0;
}

if (newComplete > End) {
newComplete = End;
}

before.style.width = newComplete + "px";
thumb.style.left = newComplete + "px";
}

function onMouseUp() {
document.removeEventListener("mouseup", onMouseUp);
document.removeEventListener("mousemove", onMouseMove);
}
};

thumb.ondragstart = function () {
return false;
};

ref

https://ko.javascript.info/mouse-drag-and-drop

script 태그의 위치

렌더링 엔진은 script태그를 다운로드하거나 실행하는 중에는 파싱을 멈춘다.
자바스크립트 파일은 css나 html을 변경시킬 가능성이 있기 때문이다.

  1. head,body태그 최상단

파싱을 시작하자마자 script 태그를 다운로드한다.
파싱을 막기 때문에 script태그를 먼저 다운로드하고 실행 후 파싱한다.
실행 순서를 정할 수 있다.
다만 존재하지 않는 DOM요소에 접근시 에러가 발생할 수 있다.

  1. body태그 최하단

파싱이 끝난 뒤에 script 태그를 다운로드 시작한다.
js파일에 의존성이 높을경우 사용자경험이 좋지 못하다.

  1. defer

병렬로 백그라운드에서 다운로드한다.
다운로드 하는 중에도 HTML 파싱을 멈추지 않는다.
defer 스크립트의 실행은 페이지 구성이 끝날 때까지 지연된다.

defer 스크립트는 DOM이 준비된 후에 실행되지만 DOMContentLoaded 전에 실행된다.

아래는 예시 코드이다.

1
2
3
4
5
6
7
8
9
<p>...스크립트 앞 콘텐츠...</p>

<script>
document.addEventListener('DOMContentLoaded', () => alert("`defer` 스크립트가 실행된 후, DOM이 준비되었습니다!")); // (2)
</script>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<p>...스크립트 뒤 콘텐츠...</p>
  1. 페이지의 콘텐츠가 모두 출력된다.

  2. DOMContentLoaded 이벤트는 defer 스크립트가 실행된 후에 발생하므로 alert창은 DOM트리가 완성되고 defer 스크립트의 실행이 완료된 후에 실행된다.

  3. async

async도 마찬가지로 백그라운드에서 다운로드 된다. 따라서 HTML 페이지는 async 스크립트 다운이 완료되길 기다리지 않고 페이지 내 컨텐츠를 출력한다.( 하지만 async 스크립트 실행중에는 HTML 파싱이 멈춘다.)

DOMContentLoaded 이벤트와 async 스크립트는 서로를 기다리지 않는다.

  • DOM 트리를 다 만든 후에 async 스크립트 다운로드가 끝났을 경우 DOMContentLoaded 이벤트는 async 스크립트 실행 전에 발생할 수 있다.
  • 마찬가지로 async 스크립트가 DOM트리를 만들기 전에 다운로드 되었을 경우 async 스크립트의 실행이 끝난 다음 DOMContentLoaded 이벤트가 발생한다.

아래는 예제 코드이다.

1
2
3
4
5
6
7
8
9
10
<p>...스크립트 앞 콘텐츠...</p>

<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM이 준비 되었습니다!"));
</script>

<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>

<p>...스크립트 뒤 콘텐츠...</p>

async 스크립트는 그 특징 떄문에 실행순서를 보장할 수 없다. 먼저 다운로드가 끝난 스크립트 순으로 실행된다. 즉 small.js가 long.js보다 먼저 실행된다.

또한 alert의 실행시점도 예측할 수 없다.

이러한 특징으로 인해 구글 어널리틱스같은 독립적인 역할을 하는 서드파티 스크립트를 삽입할 때에 유용하다. 개발중인 스크립트에 의존하지 않고 독립적으로 동작하기 때문이다.

1
<script async src="https://google-analytics.com/analytics.js"></script>

Ref

defer, async 스크립트

브라우저의 랜더링 과정에 대해 알아보자 2 최적화

size of HTML

  1. HTML의 크기를 최소로 해야한다.
  • 사전처리 및 상황별 최적화

    • 주석들을 없애야함(브라우저로 주석을 보내 줄 이유가 없다.)
    • 헤더의 인코딩방식을 다르게 한다.
    • 반복되는 데이터를 압축(AAA=>3A)
    • 중복되는 style 정의를 합친다.
    • 공백(스페이스나 탭)을 제거한다.
  1. 압축해야한다.
  • GZIP을 이용한 텍스트 압축
    • GZIP은 텍스트 기반 압축 툴이다.
    • 모든 최신 브라우저는 이를 지원하고 자도으로 요청한다.
    • 일부 CDN의 경우 GZIP이 활성화 되었는지 확인해야한다.
    • 개발자 도구 네트워크탭에서 Size/Content열을 통해 압축된 크기를 확인할 수 있다.

사전처리 및 상황별 최적화 후 GZIP을 적용하여 최소화된 출력을 압축하면 큰 절감효과를 얻을 수 있다.

  1. 브라우저에 의해 캐싱해야한다.
  • 서버를 통해 리소스를 받아오기 전까지 랜더링이 되지 않기 떄문에 캐싱을 통해 서버로의 요청을 줄이면 최적화할 수 있다.
  • 브라우저의 모든 HTTP요청은 브라우저 캐시로 라우팅되어 유효한 캐시가 있는지 먼저 확인한다.
  • 이는 요청 헤더와 응답헤더의 조합에 의해 제어된다.
  • 기본적으로 브라우저는 사용자를 대신하여 헤더설정을 관리한다.( 자동으로 캐싱한다. )
  • 웹서버에서 Cache-Control헤더를 통해 관리할 수 있다.

Unblocking CSS

  • HTML,CSS는 기본적으로 랜더링 차단 리소스이다.
  • 랜더링하려면 렌더트리가 필요하고, 렌더트리를 만들기 위해선 서버에서 CSS를 가져올 때 까지 기다려야한다.(HTML 파싱은 한다.)
  • 미디어 유형과 미디어 쿼리를 통해 일부 css를 비차단 리소스로 표시할 수 있다.
  • 혹은 inline css를 사용하여 요청을 하지 않을 수 있다.
  • 브라우저는 차단동작이든 비차단 동작이든 관계없이 모든 CSS 리소스를 다운로드 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
body {
font-size: 16px;
}

@media screen and (orientation: landscape) {
.menu {
float: right;
}
}

@media print {
body {
font-size: 12px;
}
}
  • print 파일을 분리된 파일로 옯긴다.
  • 미디어 속성을 통해 해당 css가 언제 적용될 지 브라우저에게 전달할 수 있다.(비차단 리소스로 표시하기)
1
2
3
4
5
6
7
8
9
body {
font-size: 16px;
}

@media screen and (orientation: landscape) {
.menu {
float: right;
}
}
1
2
3
4
5
@media print {
body {
font-size: 12px;
}
}
1
2
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="style-print.css" media="print">

JavaScript and CRP

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/adding-interactivity-with-javascript#parser-blocking-vs-asynchronous-javascript

  • JS 는 DOM과 CSSOM 모두 조작 가능

  • script 태그를 만났을 때는 DOM 생성을 중단하고 자바스크립트가 실행되기를 기다려야한다

  • script 태그가 DOM 생성을 막기 때문에 js는 parser blocking이다.

  • 인라인 스크립트 대신 외부파일로 스크립트를 사용할 경우엔 파서가 script 태그를 발견하면 파일을 받아서 실행한다. 파일을 가져오는 동안에 DOM 생성을 계속할 수 없으므로 CRP가 늦어진다.

  • 인라인 스크립트를 사용하는 것은 요청을 줄이는 데에 도움을 주지만 코드가 반복되고 과도하게 사용될 수 있는 단점이 있다.

  • js는 CSS를 조작할 가능성도 있기 때문에 script는 css가 도착하고 CSSOM을 생성할 때 까지 실행되지 않는다.(CSSOM 생성 후 JS가 실행된다.) 따라서 js 최적화는 css최적화와 깊은 연관이 있다.

  • 사용자 분석 등 랜더링에 영향을 주지 않는 스크립트

    • 랜더링이 끝나고 브라우저가 onload 이벤트를 발생 시켰을 때 실행하여 최적화 할 수 있다.
    • script 태그에 async 속성(CRP를 막지 않는 script)을 통해 최적화 할 수 있다. async 속성을 붙일경우 브라우저의 DOM 생성을 막지 않는다. script 요청을 처리하고 dom을 파싱한다. 또한 CSSOM에 영향을 받지 않는다. CSSOM생성 전에 script를 사용 가능하다면 바로 실행할 수 있다.
    1
    <script src="analytics.js" async></script>
    • 자바스크립트를 css위에 넣어서 css에 방해받지 않고 실행할 수 있다.

요약

  • 데이터통신의 양을 줄임
  • 중요한 리소스의 개수를 줄임
  • CRP의 길이를 줄인다
    • 서버와 클라이언트를 왕복하는 수

Preload Scanner

  • 문서의 앞부분에서 CSS나 js를 알아내려 한다.
  • 파서가 막힌동안 js나 css를 다운로드받는다.(병렬 요청)

브라우저의 랜더링 과정에 대해 알아보자 1 CRP

브라우저의 랜더링 과정과 CRP에 대해 알아보자

Critical Rendering Path

  • 브라우저가 HTML,CSS,Javascript를 화면에 실제 픽셀로 변환하는 단계의 순서.
  1. HTML을 가져와서 DOM(Document Object Model)을 생성한다.
  2. CSS를 가져와서 CSSOM(CSS Object Model)을 생성한다.
  3. 둘을 합쳐서 Render Tree를 만든다.
  4. 레이아웃 단계를 거쳐서 모든 것이 페이지의 어느 위치에 갈 것인지 정한다.
  5. 실제 화면에 픽셀을 그린다.

Converting HTML to the DOM

  • URL을 요청하고 엔터를 누르면 브라우저가 서버로 HTML 요청을 보낸다.
  1. 브라우저가 HTML을 읽어와서 지정된 인코딩에 따라 개별문자로 변환한다.
  2. HTML은 태그를 만날 때 마다 W3C HTML5표준에 맞춰 토큰을 만든다.
  3. 토큰은 해당 속성 및 규칙을 정의하는 객체로 변환된다.
  4. HTML 마크업 간의 관계를 정의하기 떄문에 생성된 객체는 트리 데이터 구조 내에 연결된다.(노드가 된다.) 이는 상하위 관계도 포함한다. 즉 HTML객체는 body의 상위이고, body는 paragraph 객체의 상위이다.

이러한 구조로 부분 HTML을 먼저 로드하여 성능개선을 할 수 있다.

Converting CSS to the CSSOM

  1. 유효한 토큰이 존재하는지 확인한다.
  2. 토큰을 노드화한다.

부분 css의 사용은 불가능하다. 잘못된 스타일을 사용 할 수 있기 때문이다.
브라우저는 모든 CSS를 받고 처리할 때 까지 페이지 랜더링을 차단한다.

  • 선택자의 차이
1
2
3
4
5
6
h1 {
font-size: 16px;
}
div p {
font-weight: 12px;
}

첫 번째 태그는 h1에 일괄적으로 적용하지만 두 번째 태그는 모든 p 태그를 찾고
부모노드가 div인 것을 찾아서 스타일을 적용하기 때문에 브라우저가 더 많은 작업을 해야한다.

The Render Tree

  • DOM 과 CSSOM트리를 합친 트리이다.
  • 눈에 보이는 내용만 포함한 트리이다.
    • display:none과 같은 style이 있는 노드의 경우 포함되지 않는다.
    • html태그는 눈에 보이는 내용이 없으므로 제거한다.

Layout

  • width
    • %의 경우 메타태그에서 설정한 뷰포트의 너비 or 부모의 너비를 상속한다.
    • 아래의 태그의 경우 뷰포트 너비를 디바이스의 너비로 설정한다. 기본값은 980px이다.
      1
      <meta name="viewport" content="width=device-width">
  • 스타일이나 내용을 변경하여 렌더트리를 업데이트 할 경우 레이아웃 단계를 다시 실행할 가능성이 크다.
  • 뷰포트 너비가 변하면 브라우저는 레이아웃 단계를 다시 시행해야 한다.
    (폰을 돌리거나 브라우저 크기를 조정할 떄 일어난다.)

이를 피하기 위해 여러번의 레이아웃 이벤트를 피하고자 업데이트를 한 번에 반영하는 것이다.

redux-saga runAllEffect 에러 해결하기

클론코딩을 하던 중 이런 에러를 마주하였다.

스크롤을 내려보면 runAllEffect에서 발생한 에러임을 알 수 있다.
TypeError: Cannot read property ‘forEach’ of null

runAllEffect 에러

해결방안

액션을 구독할 때에 take함수를 쓰는데, import를 해주지 않아서 발생한 에러였다. take를 import 해주었다.
자바스크립트라 어이없는 실수가 많다..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { all, delay, fork, take } from "redux-saga/effects";

// function logInAPI(data) {
// return axios.post("/api/login");
// }

function* logIn(data) {
try {
// const result = yield call(logInAPI, action.data);
yield delay(1000);
yield put({
type: "LOG_IN_SUCCESS",
data: action.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}

function* logOut() {
try {
yield delay(1000);
yield put({
type: LOG_OUT_SUCCESS,
});
} catch (err) {
console.error(err);
yield put({
type: LOG_OUT_FAILURE,
error: err.response.data,
});
}
}

function* watchLogIn() {
yield take("LOG_IN_REQUEST", logIn);
}

function* watchLogOut() {
yield take("LOG_OUT_REQUEST", logOut);
}

export default function* userSaga() {
yield all([fork(watchLogIn), fork(watchLogOut)]);
}
Server Error TypeError: Cannot read property 'forEach' of null

This error happened while generating the page. Any console logs will be displayed in the terminal window.
Call Stack
end
file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (1017:18)
Object.abort
file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (817:5)

file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (598:22)
immediately
file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (60:12)
runForkEffect
file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (588:3)
runEffect
file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (1208:7)
digestEffect
file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (1275:5)

file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (677:5)
Array.forEach

runAllEffect
file:///C:/Users/SDPark-pc/Desktop/REACT/nodebird/front/node_modules/@redux-saga/core/dist/redux-saga-core.dev.cjs.js (676:8)

React로 NodeBird SNS 만들기 3 리덕스 적용하기

재 밌 다!

재밌어서 색상을 바꾸고 강조해보았다(div태그 이용).
오랜만에 리액트를 만지니까 재밌다.
오늘은 프로젝트에 리덕스를 적용하였다.
next에는 redux를 적용할 수 있도록 도와주는 고마운 라이브러리가 있다.
또한 기존의 redux와 달리 라이브러리에서 자동으로 provider로 감싸주기 때문에 provider로 감싸지 않아도 된다.

설치하기

라이브러리들을 설치해준다.

1
2
3
npm i next-redux-wrapper로 설치
npm i redux
npm i react-redux

리듀서 작성하기

먼저 루트 디렉터리에 reducer 폴더를 만든다.

위 reducer 폴더에 user.js파일을 만들고 개요를 작성해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
export const initialState = {
isLoggedIn: false,
user: null,
signUpData: {},
loginData: {},
};

export const loginAction = (data) => {
return {
type: "LOG_IN",
data,
};
};

export const logoutAction = () => {
return {
type: "LOG_OUT",
};
};

const reducer = (state = initialState, action) => {
switch (action.type) {
case "LOG_IN": {
return {
...state,
user: action.data,
isLoggedIn: true,
};
}
case "LOG_OUT": {
return {
...state,
user: null,
isLoggedIn: false,
};
}
default:
return state;
}
};

export default reducer;

마찬가지로 reducer 폴더에 post.js파일을 만들고 개요를 작성해준다.

1
2
3
4
5
6
7
8
9
10
11
12
export const initialState = {
mainPosts: [],
};

const reducer = (state = initialState, action) => {
switch (action.type) {
default:
return state;
}
};

export default reducer;

마지막으로 해당 경로에 index.js파일을 만들고
리듀서들을 합쳐준다.
HYDRATE는 서버사이드 랜더링을 위한 것이라고 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { HYDRATE } from "next-redux-wrapper";
import user from "./user";
import post from "./post";
import { combineReducers } from "redux";
const initialState = {};

const rootReducer = combineReducer({
index: (state = {}, action) => {
switch (action.type) {
case HYDRATE:
return { ...state, ...action.payload };
default: {
return state;
}
}
},
user,
post,
});
export default rootReducer;

스토어 만들기

루트 디렉토리에 store폴더를 만들고 configureStore.js 파일을 작성해준다.
redux devtools또한 적용하였다.
개발모드일 때랑 배포모드일 때 미들웨어를 다르게 하였다.
마지막으로 래퍼를 생성해 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createWrapper } from "next-redux-wrapper";
import { applyMiddleware, compose, createStore } from "redux";
import reducer from "../reducers";
import { composeWithDevTools } from "redux-devtools-extension";

const configureStore = () => {
const middlewares = [];
const enhancer =
process.env.NODE_ENV === "production"
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(reducer, enhancer);
return store;
};

const wrapper = createWrapper(configureStore, {
debug: process.env.NODE_ENV === "development",
});

export default wrapper;

적용하기

가장 상위 파일인 app.js를 다음과 같이 감싸준다.
그러면 라이브러리에서 provider로 자동으로 감싸준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import PropTypes from "prop-types";
import "antd/dist/antd.css";
import Head from "next/head";
import wrapper from "../store/configureStore";

const App = ({ Component }) => {
return (
<>
<Head>
<title>NodeBird</title>
<meta charSet="utf-8" />
</Head>
<Component />
</>
);
};

App.propTypes = {
Component: PropTypes.elementType.isRequired,
};

export default wrapper.withRedux(App);

사용하기

기존 redux와 동일하게 useSelector, useDispatch를 사용할 수 있다.

React로 NodeBird SNS 만들기 2 redux-saga

리덕스에 비동기 작업들을 처리하는 리덕스 사가를 사용해보자!
본 게시글은 next.js +redux + redux-saga기준으로 작성하였다.

설치

1
npm i redux-saga

Store에 적용하기

Store를 만들고 미들웨어를 추가해준다.
그 후 반환 전 사가 미들웨어를 실행해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { createWrapper } from "next-redux-wrapper";
import { applyMiddleware, compose, createStore } from "redux";
import reducer from "../reducers";
import { composeWithDevTools } from "redux-devtools-extension";
import createSagaMiddleware from "redux-saga";

const configureStore = () => {
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const enhancer =
process.env.NODE_ENV === "production"
? compose(applyMiddleware(...middlewares))
: composeWithDevTools(applyMiddleware(...middlewares));
const store = createStore(reducer, enhancer);
store.sagatask = sagaMiddleware.run(rootSaga);
return store;
};

const wrapper = createWrapper(configureStore, {
debug: process.env.NODE_ENV === "development",
});

export default wrapper;

제너레이터

중단점이 있는 함수이다 next()를 통해 다음 중단점 전까지 실행한다.
return된 값은 value에 저장된다.
saga에서는 이벤트 리스너로 활용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const func = function* () {
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
yield;
};

func; //func{<suspended>}
func.next(); //1 {value:undefined,done:false}
func.next(); //2 {value:undefined,done:false}
func.next(); //3 {value:undefined,done:true}

saga에서의 제너레이터 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}

function* watchLogIn() {
yield take("LOG_IN_REQUEST", logIn);
}

function* watchLogOut() {
yield take("LOG_OUT_REQUEST");
}

export default function* rootSata() {
yield all([fork(watchLogIn), fork(watchLogOut)]);
}

saga 이펙트

sagas/index.js에 이펙트를 작성하였다.

all

  • 배열안의 함수를 동시에 실행한다
1
2
3
export default function* rootSata() {
yield all([fork(watchLogIn), fork(watchLogOut)]);
}

fork

  • 매개변수로 받은 함수를 비동기 실행하는 함수이다.
1
2
3
export default function* rootSata() {
yield all([fork(watchLogIn), fork(watchLogOut)]);
}

call

  • 매개변수로 받은 함수를 동기 실행하는 함수이다.
  • call(함수,매개변수1,매개변수2,…)의 형식으로 호출해주어야 한다.
  • 아래 함수의 경우 call 아래줄은 500ms동안 지연이 일어난 후에 실행된다.
1
2
3
4
function* handleInput(input) {
// 500ms마다 지연
yield call(delay, 500);
}

take

  • 해당 액션이 실행될 때 까지 기다리는 함수이다.
  • LOG_IN_REQUEST 액션 발생시 logIn함수를 실행한다.
1
2
3
function* watchLogIn() {
yield take("LOG_IN_REQUEST", logIn);
}

put

  • store에게 dispatch하는 이펙트이다.
  • saga 미들웨어가 실행된 store에 전달한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* logIn(action) {
try {
const result = yield call(logInAPI, action.data);
yield put({
type: "LOG_IN_SUCCESS",
data: result.data,
});
} catch (err) {
yield put({
type: "LOG_IN_FAILURE",
data: err.response.data,
});
}
}

saga에서의 제너레이터 적용

  • 액션별로 함수를 지정해주고 액션이 발생시 그 함수를 호출해준다.
    이펙트 앞에는 yield를 붙여주어야 보장된다.

  • rootSaga에서 해당 이벤트가 발생시 핸들러에게 action을 같이 넘긴다.
    기존 redux와 같이 action.data, action.type로 이벤트 타입과 같이 넘겨진 데이터에 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
import { all, fork, take, call, put } from "redux-saga/effects";
import axios from "axios";

function logInAPI(data) {
return axios.post("/api/login", data);
}

function* watchLogOut() {
yield take("LOG_OUT_REQUEST");
}

기타 saga 이펙트

take

  • 일회용이다. 한 번 이벤트를 받으면 사라진다.
  • while로 감싸 계속 사용할 수 있다.
1
2
3
4
5
function* watchLogOut() {
while (true) {
yield take("LOG_OUT_REQUEST");
}
}

takeEvery

  • 계속해서 사용할 수 있다.
  • while문을 대체할 수 있고, while문은 동기적으로 동작하지만 takeEvery는 비동기적으로 동작한다.
1
2
3
function* watchLogOut() {
yield takeEvery("LOG_OUT_REQUEST");
}

delay

  • 해당 시간동안 대기한다.
1
2
3
function* watchLogOut() {
yield delay(1000); //1초 대기
}

debounce

  • 연이어 호출되는 함수들 중 마지막 함수만 호출되도록 한다. 호출스택의 제일 윗부분의 요청이 실행되면 값이 task에 저장되어 나머지가 전부 cancel된다.

delay 이용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* handleInput(input) {
// 500ms마다 지연
yield call(delay, 500)
...
}
function* watchInput() {
let task;
while (true) {
const { input } = yield take("INPUT_CHANGED");
if (task) {
yield cancel(task);
}
task = yield fork(handleInput, input);
}
}

takeLatest 이용

  • 마지막 요청만 인식하여 한 번만 실행한다.
1
2
3
4
5
6
7
8
9
10
11
12
import { delay } from 'redux-saga'

function* handleInput({ input }) {
// 500ms마다
yield call(delay, 500)
...
}

function* watchInput() {
// 현재 실행중인 handleInput 작업을 취소합니다.
yield takeLatest('INPUT_CHANGED', handleInput);
}

throttle

  • 마지막 함수가 호출된 후 일정 시간이 지나기 전에 다시 호출되지 않도록 한다.
1
2
3
4
5
6
7
function* handleInput(input) {
// ...
}

function* watchInput() {
yield throttle(500, "INPUT_CHANGED", handleInput);
}

ref

https://mskims.github.io/redux-saga-in-korean/recipes/

React로 NodeBird SNS 만들기 1

인프런에서 제로초(조현영)님이 진행하시는 React로 NodeBird SNS 만들기을 보고 따라해보기로 했다.

이전에 한 번 진행하였던 과정인데, node+react 기반의 풀스택 프로젝트 경험이 필요하여 진행하게 되었다.

매일매일 조금씩 진행해볼 예정이다.

시작하기 전에 몇 개의 원칙을 정하였다.

원칙

  1. 프로젝트에 git flow 적용하기
  2. 개선점을 찾아 개선해보기
  3. 짧게라도 계속해서 기록으로 남기기( 다만 공식문서를 보고 알 수 있는 내용은 배제)

사용된 라이브러리

폴더 구조는 다음과 같다.
page구조

  1. components

    여러 컴포넌트들을 정의하였다.

    레이아웃의 공통부분인 AppLayout.js을 정의하고 이를 각 페이지에서 받아와서 썼다.

  2. pages

    클론코딩에는 next.js가 사용되었다.
    next.js는 js 프레임워크인 리액트의 프레임워크이다.
    즉 js 프레임워크의 프레임워크이다.
    때문에 여러 편리한 기능들을 제공한다.

    next js는 루트폴더의 pages라는 폴더 내부의 파일명으로 spa를 만든다.
    나는 front 페이지에서 next를 설치하고 작업하였다.

    page구조

    pages/index.js파일은 현재 경로에 아무것도 없을 때 실행된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import AppLayout from "../components/AppLayout";

    const Home = () => {
    return (
    <AppLayout>
    <div>Hello, Next!</div>
    </AppLayout>
    );
    };

    export default Home;

    pages/_app.js파일은 여러 파일에 공통적인 부분들을 적는 파일이다.
    index.js에서 export된 내용들이 props.Component로 들어온다.
    next에서 제공하는 Head 태그를 사용하여 head태그를 수정하였다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import PropTypes from "prop-types";
    import "antd/dist/antd.css";
    import Head from "next/head";

    const App = ({ Component }) => {
    return (
    <>
    <Head>
    <title>NodeBird</title>
    <meta charSet="utf-8" />
    </Head>
    <Component />
    </>
    );
    };

    App.propTypes = {
    Component: PropTypes.elementType.isRequired,
    };

    export default App;

    나머지 파일들은 /파일명 으로 접근할 수 있다.

생각난 개선점

  1. Head 태그
    next에서 제공하는 Head 태그를 이용하여 여러 meta 태그들을 직접 다루어보고 적용해주고 싶다..!

  2. Login Form에 라이브러리 적용하기
    react-hook-form과 같은 validation 라이브러리를 적용하여 안정성을 더하고 싶다.

  3. 시멘틱 태그 적용하기
    구현이 어느정도 되었을 때의 이야기지만, 시맨틱 태그를 적용하여 검색엔진에 쉽게 걸리도록 하고 싶다.

  4. typescript 적용하기
    (이건 온전히 내 타입스크립트 실력을 위해서) 아직 타입스크립트가 낯설다 ㅠ
    특히 제네릭을 사용하는 부분은 거의 외워서 작성하게 된다.
    연습을 위해 타입스크립트를 적용해보아야겠다..! 이것도 나중에!

  5. BEM 적용하기
    BEM 네이밍 방식의 연습을 위해 컴포넌트 wrapper에는 styled-components를 사용하고 아래에는 BEM네이밍 방식을 적용할 생각이다.

느낀점

예전에 했을 때는 진짜 따라가기에 벅찼는데, 지금 다시 해보니 새로운 부분들이 많이 보인다.
최대한 열심히 많은 것을 얻어갈 수 있도록 진행해보아야겠다!

깃 커밋 컨벤션에 대해 알아보기

라고 호기롭게 글을 적었으나,,

여러 글을 참조하던 중 이미 예쁘게 정리해주신 분의 아티클이 있어 공유합니다.

저도 이 분처럼 처음 보는 입장에서도 쉽게 이해가 가는 예쁜 글을 적고싶어요.

갈 길이 멀어요..!

깃 커밋 컨벤션에 대해 알아보기