An open API service indexing awesome lists of open source software.

https://github.com/ssi02014/blog

๐Ÿ’ป MERN ์Šคํƒ์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด๋Š” ๋‚˜๋งŒ์˜ ๋ธ”๋กœ๊ทธ
https://github.com/ssi02014/blog

Last synced: 8 months ago
JSON representation

๐Ÿ’ป MERN ์Šคํƒ์œผ๋กœ ๋งŒ๋“ค์–ด๋ณด๋Š” ๋‚˜๋งŒ์˜ ๋ธ”๋กœ๊ทธ

Awesome Lists containing this project

README

          

# ๐Ÿ’ป Blog
### MERN(Mongodb, Express, React, Node) Stack์œผ๋กœ ๋งŒ๋“  ๋‚˜๋งŒ์˜ ๋ธ”๋กœ๊ทธ๐Ÿ˜


## ๐Ÿ™ Blog App ์ข…์†์„ฑ ๋‹ค์šด๋กœ๋“œ
1. **" npm i or yarn install "** ์„ server ํด๋”์—์„œ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. **(๋ฐฑ์—”๋“œ ์ข…์†์„ฑ ๋‹ค์šด๋ฐ›๊ธฐ)**
2. **" npm i or yarn install "** ์„ client ํด๋”์—์„œ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. **(ํ”„๋ก ํŠธ์—”๋“œ ์ข…์†์„ฑ ๋‹ค์šด๋ฐ›๊ธฐ)**
3. **.env** ํŒŒ์ผ์„ server ํด๋” ๋‚ด๋ถ€์— ๋งŒ๋“ค์–ด์ฃผ์…”์•ผ ๋ฉ๋‹ˆ๋‹ค. **(๋ฐ‘์— ์ฐธ๊ณ )๐Ÿ‘‡**
4. **.env** ํŒŒ์ผ์„ client ํด๋” ๋‚ด๋ถ€์— ๋งŒ๋“ค์–ด์ฃผ์…”์•ผ ๋ฉ๋‹ˆ๋‹ค. **(๋ฐ‘์— ์ฐธ๊ณ )๐Ÿ‘‡**


## ๐Ÿ”– Main Development Stack
### ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Backend
1. Node.js
2. Express
3. MongoDB


### ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Frontend
1. React
2. Redux, Redux-Saga
3. Infinite Scroll(Intersection Observer)
4. Ckeditor5
5. Styling: Bootstrap4(reactstrap), SCSS


### ๐Ÿ‘จ๐Ÿปโ€๐Ÿ’ป Cloud
1. Aws EC2
2. Aws S3


## ๐Ÿ“ƒ ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€
![1111](https://user-images.githubusercontent.com/64779472/121060969-3df4b500-c7fe-11eb-8a28-8d4b152c6f78.PNG)

- Add: ํŠน์ • ๊ธฐ๋Šฅ์„ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€์„ ๋•Œ
- Modify: ์ด๋ฏธ ๊ตฌํ˜„๋œ ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•˜๋Š”๋ฐ, ๊ธฐ๋Šฅ์˜ ํ–ฅ์ƒ์ด ์ด๋ฃจ์–ด์กŒ์„ ๋•Œ
- Close(Closes, Closed): ์ผ๋ฐ˜์ ์ธ ๊ฐœ๋ฐœ ์ด์Šˆ๋ฅผ ์™„๋ฃŒํ–ˆ์„ ๋•Œ
- Refactor: ๋ฆฌํŒฉํ† ๋ง ํ–ˆ์„ ๋•Œ(๊ธฐ๋Šฅ ํ–ฅ์ƒ์€ ์•„๋‹ˆ๋‹ค. ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ ๋ฐ ๋ณ€์ˆ˜ & ํ•จ์ˆ˜ ๋“ฑ ์ฝ”๋“œ ๋””์ž์ธ ๋ณ€๊ฒฝ)
- Delete: ๋ถˆํ•„์š”ํ•œ ์ฝ”๋“œ ์ œ๊ฑฐ
- Fix(Fixex, Fixed): ๋ฒ„๊ทธ ํ”ฝ์Šค๋‚˜ ํ•ซ ํ”ฝ์Šค ์ด์Šˆ๋ฅผ ์™„๋ฃŒํ–ˆ์„ ๋•Œ
- Merge: Branch๋ฅผ merge ํ–ˆ์„ ๋•Œ
- Conflict: ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ–ˆ์„ ๋•Œ
- Docs: README.md์™€ ๊ฐ™์€ ๋ฌธ์„œ ์ˆ˜์ •ํ–ˆ์„ ๋•Œ


## ๐Ÿ“– Projects Board
![๊ทธ๋ฆผ1](https://user-images.githubusercontent.com/64779472/120682567-91ea5b80-c4d7-11eb-9d39-c6dbe0643446.png)


## ๐Ÿ“ˆ Server: ํ•™์Šต ๋‚ด์šฉ ๋ฐ ์ด์Šˆ
### ๐Ÿ” 1. Server์—์„œ Babel ํ™˜๊ฒฝ ์„ค์ •
- **Babel**: ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ปดํŒŒ์ผ๋Ÿฌ, ์ตœ์‹  ๋ฒ„์ „์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฌธ๋ฒ•์€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ดํ•ดํ•˜์ง€ ๋ชปํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋Š” ๋ฌธ๋ฒ•์œผ๋กœ ๋ณ€ํ™˜ํ•œ๋‹ค.
```javascript
// ./server/.babelrc
// ์„œ๋ฒ„์—์„œ ๋ฐ”๋ฒจ ํ™˜๊ฒฝ ์„ค์ •
{
"presets": ["@babel/preset-env"]
}
```


### ๐Ÿ” 2. Mongoose ์˜ค๋ฅ˜ ๋ฐ ํ•ด๊ฒฐ์ฑ…
1. **mongoose version 5.11.16์œผ๋กœ ์ธํ•œ ์˜ค๋ฅ˜๐Ÿ’ฅ**
```
- DeprecationWarning: Listening to events on the Db class has been deprecated and will be removed in the next major version.
- โœ… npm install mongoose@5.11.15 (๋ฒ„์ „ ๋‚ฎ์ถ”๊ธฐ)
```


## ๐Ÿ” 3. sever: .env
```js
//๋ณธ์ธ์˜ mongoDB cluster ์ƒ์„ฑ ์‹œ์— ๋งŒ๋“  connection URI๋ฅผ ๋„ฃ์–ด์ฃผ์„ธ์š”.
MONGO_URI = "mongodb+srv://:@blog.io9gx.mongodb.net/myFirstDatabase?retryWrites=true&w=majority"

PORT="7000" //server๋ฅผ ์‹คํ–‰์‹œํ‚ฌ port

JWT_SECRET = "Minjae" //์•„๋ฌด ๋ฌธ์ž์—ด์ด๋‚˜ ๋„ฃ์œผ์…”๋„ ์ƒ๊ด€์—†์Šต๋‹ˆ๋‹ค.
```

## ๐Ÿ” 4. client: .env
```js
REACT_APP_BASIC_SERVER_URL = "http://localhost:7000"
REACT_APP_BASIC_IMAGE_URL = "https://<๋ณธ์ธs3๋ฒ„ํ‚ท>.s3.ap-northeast-2.amazonaws.com/<๋ฒ„ํ‚ท ์† ๊ฐ์ฒด ํด๋”๋ช…(ex.upload)>/<์ด๋ฏธ์ง€URL>"
```

## ๐Ÿ” 5. .env ์ฃผ์˜ ์‚ฌํ•ญ
```
1. React์—์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ์ ‘๋‘์‚ฌ๋กœ REACT_APP_ ๋„ฃ์–ด์•ผ๋œ๋‹ค.
2. ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๋ฐ˜์˜ํ•˜๋ ค๋ฉด ์„œ๋ฒ„๋ฅผ ๋‹ค์‹œ ์‹œ์ž‘ํ•ด์•ผ ๋œ๋‹ค.
3. src ํด๋”๊ฐ€ ์•„๋‹Œ root ํด๋” ์ฆ‰(package.json๊ณผ ๋™์ผํ•œ ์œ„์น˜)์— ์žˆ์–ด์•ผ ํ•œ๋‹ค.
```


### ./confing/index.js๋กœ .env ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
```javascript
import dotenv from 'dotenv';

dotenv.config();

export default {
MONGO_URI: process.env.MONGO_URI,
JWT_SECRET: process.env.JWT_SECRET,
PORT: process.env.PORT,
}
```




## ๐Ÿ“ˆ Client: ํ•™์Šต ๋‚ด์šฉ ๋ฐ ์ด์Šˆ
### ๐Ÿ” 1. node-sass ์ตœ์‹  ๋ฒ„์ „ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ์ฑ…
```
- โœ… npm install node-sass@4.14.1
```


### ๐Ÿ” 2. node-sass ์ตœ์‹  ๋ฒ„์ „ ์˜ค๋ฅ˜ ํ•ด๊ฒฐ์ฑ…
```
- ๐Ÿ’ฅError: Could not find router reducer in state tree, it must be mounted under "router"
- ์ด์œ : connected-react-router๊ฐ€ ์•„์ง history v5๋ฅผ ์ œ๋Œ€๋กœ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•ด ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ.

- โœ… npm i history@4.7.2 (๋ฒ„์ „ ๋‚ฎ์ถ”๊ธฐ)
```


### ๐Ÿ” 3. reactstrap ์‚ฌ์šฉ๋ฒ•
### reactstarp: https://reactstrap.github.io/
```javascript
//Header
import { Row, Col } from 'reactstrap';

//class์— text-center, m-auto ๊ฐ™์ด ์ถ”๊ฐ€ํ•ด์„œ ์ ์šฉ์‹œํ‚จ๋‹ค.


Read Our Blog


This is Minjae's Side Project Blog




```


### ๐Ÿ” 4. font-awesome ์‚ฌ์šฉ๋ฒ•
```javascript
1. yarn add or npm i @fortawesome/fontawesome-svg-core
2. yarn add or npm i @fortawesome/free-solid-svg-icons
3. yarn add or npm i @fortawesome/react-fontawesome

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faMouse } from '@fortawesome/free-solid-svg-icons';


```


### ๐Ÿ” 4. CKEditor5 Setting
### CKEditor:https://ckeditor.com/docs/ckeditor5/latest/builds/guides/integration/frameworks/react.html


```javascript
1. npm run eject (์ด์ „ ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ commit ์™„๋ฃŒ๋˜์•ผํ•จ)

2. yarn add @babel/plugin-transform-react-jsx @babel/plugin-transform-react-jsx-self

3. plugin Install
- yarn add @ckeditor/ckeditor5-adapter-ckfinder @ckeditor/ckeditor5-alignment @ckeditor/ckeditor5-autoformat @ckeditor/ckeditor5-basic-styles @ckeditor/ckeditor5-block-quote @ckeditor/ckeditor5-build-balloon @ckeditor/ckeditor5-build-classic @ckeditor/ckeditor5-build-inline @ckeditor/ckeditor5-dev-utils @ckeditor/ckeditor5-dev-webpack-plugin @ckeditor/ckeditor5-easy-image @ckeditor/ckeditor5-editor-balloon @ckeditor/ckeditor5-editor-classic @ckeditor/ckeditor5-essentials @ckeditor/ckeditor5-font @ckeditor/ckeditor5-heading @ckeditor/ckeditor5-image @ckeditor/ckeditor5-indent @ckeditor/ckeditor5-link @ckeditor/ckeditor5-list @ckeditor/ckeditor5-media-embed @ckeditor/ckeditor5-paragraph @ckeditor/ckedito5-paste-from-office @ckeditor/ckeditor5-react @ckeditor/ckeditor5-table @ckeditor/ckeditor5-theme-lark @ckeditor/ckeditor5-typing @ckeditor/ckeditor5-upload

4. webpack.config.js Setting
- webpack: ์›น์„ ๊ตฌ์„ฑํ•˜๋Š” .js, .css, .jpg, .png ๊ฐ™์€ static assets์„ ํ•˜๋‚˜์˜ ํŒŒ์ผ๋กœ ํ•ฉ์ณ์คŒ

//webpack.config.js 34๋ฒˆ์งธ ์ค„์— ์†Œ์Šค ์ถ”๊ฐ€
const { styles } = require('@ckeditor/ckeditor5-dev-utils');
const CKEditorWebpackPlugin = require("@ckeditor/ckeditor5-dev-webpack-plugin");

//webpack.config.js 467๋ฒˆ์งธ ์ค„์— ์†Œ์Šค ์ถ”๊ฐ€ (inputSource ๊ฒ€์ƒ‰)
{
test: /ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/,
use: [ 'raw-loader' ]
},
{
test: /ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/,
use: [
{
loader: 'style-loader',
options: {
injectType: 'singletonStyleTag',
attributes: {
'data-cke': true
}
}
},
{
loader: 'postcss-loader',
options: styles.getPostCssConfig( {
themeImporter: {
themePath: require.resolve( '@ckeditor/ckeditor5-theme-lark' )
},
minify: true
} )
}
]
},

//webpack.config.js 505๋ฒˆ์งธ ์ค„์— ์†Œ์Šค ์ถ”๊ฐ€ (cssRegex ๊ฒ€์ƒ‰)
{
test: cssRegex,
exclude: [
cssModuleRegex,
/ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/,
],
}

//webpack.config.js 527๋ฒˆ์งธ ์ค„์— ์†Œ์Šค ์ถ”๊ฐ€ (cssModuleRegex ๊ฒ€์ƒ‰)
{
test: cssModuleRegex,
exclude: [
/ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/,
],
}

//webpack.config.js 585๋ฒˆ์งธ ์ค„์— ์†Œ์Šค ์ถ”๊ฐ€
{
loader: require.resolve( 'file-loader' ),
// Exclude `js` files to keep the "css" loader working as it injects
// its runtime that would otherwise be processed through the "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpack's internal loaders.
exclude: [
/\.(js|mjs|jsx|ts|tsx)$/,
/\.html$/,
/\.json$/,
/ckeditor5-[^/\\]+[/\\]theme[/\\]icons[/\\][^/\\]+\.svg$/,
/ckeditor5-[^/\\]+[/\\]theme[/\\].+\.css$/
],
options: {
name: 'static/media/[name].[hash:8].[ext]',
}
}

5. EdiotrConfig.js ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ์†Œ์Šค ์ถ”๊ฐ€
- 'https://www.ssaple.net/posts/5ef0b5c69e4ac10611c45a57' ์ฐธ๊ณ 

6. UploadAdaper.js ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ์†Œ์Šค ์ˆ˜์ •
- 'https://ckeditor.com/docs/ckeditor5/latest/framework/guides/deep-dive/upload-adapter.html#the-complete-implementation' ์ฐธ๊ณ 

_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open('POST', `http://localhost:7000/api/post/image`, true ); //์—ฌ๊ธฐ ์ˆ˜์ •
xhr.responseType = 'json';
}

// funtcion MyCustomUploadAdapterPlugin -> My init(ํ™”์‚ดํ‘œ ํ•จ์ˆ˜)
const Myinit = ( editor ) => { //๊ทธ๋ƒฅ ํ•จ์ˆ˜์—์„œ ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๋กœ ์ˆ˜์ •
editor.plugins.get( 'FileRepository' ).createUploadAdapter = ( loader ) => {
return new MyUploadAdapter( loader );
};
}
export default Myinit;
```

### ๐Ÿ” 5. CKEditor5 ์‚ฌ์šฉ ๋ฐฉ๋ฒ• ๋ณ€๊ฒฝ
```javascript
//๋ณ€๊ฒฝ ์ „
import CKEditor from '@ckeditor/ckeditor5-react';

//๋ณ€๊ฒฝ ํ›„
import { CKEditor } from '@ckeditor/ckeditor5-react';
```


```javascript
//๋ณ€๊ฒฝ ์ „

//๋ณ€๊ฒฝ ํ›„

```


### ๐Ÿ” 6. CKEditor5 Styling
```scss
//CKEditor5 Setting
.ck {
.ck-editor {
min-width: 100%;
}
}

.ck-editor__editable {
max-height: 80rem;
min-height: 40rem;
min-width: 100%;
}

.ck-editor__editable_inline {
max-height: 80rem;
min-height: 40rem;
min-width: 100%;
}
```

### ๐Ÿ” 7. ProtectRoute ๊ตฌํ˜„
```javascript
export const EditProtectedRoute = ({component: Component, ...rest}) => {
const {userId} = useSelector(state => state.auth);
const {creatorId} = useSelector(state => state.post);
return (
{
if(userId === creatorId) {
return
} else {
return (

)
}
}}
/>
)
}
```


### ๐Ÿ” 8. comment input value reset
```javascript
1. useRef ์‚ฌ์šฉ
const resetValue = useRef(null);

2. input์—๋‹ค Ref ๋„ฃ๊ธฐ

3. resetValue.current.value ์ดˆ๊ธฐํ™” ๋ฐ formValue()์ดˆ๊ธฐํ™”
const onSubmit = async e => {

(...)

resetValue.current.value = '';
setformValue("");
}
```



## ๐Ÿƒ Redux
### ๐Ÿ” 1. Redux-Saga Effect
```
1. put: ํŠน์ • ์•ก์…˜์„ dispatch ํ•œ๋‹ค.
2. takeEvery: ๋“ค์–ด์˜ค๋Š” ๋ชจ๋“  ์•ก์…˜์— ๋Œ€ํ•ด ํŠน์ • ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•ด์ค€๋‹ค.
3. takeLatest: ๊ธฐ์กด์— ์ง„ํ–‰ ์ค‘์ด๋˜ ์ž‘์—…์ด ์žˆ๋‹ค๋ฉด ์ทจ์†Œํ•˜๊ณ , ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์œผ๋กœ ์‹คํ–‰๋œ ์ž‘์—…๋งŒ ์ˆ˜ํ–‰ํ•œ๋‹ค.
4. all: ์—ฌ๋Ÿฌ ์‚ฌ๊ฐ€๋ฅผ ํ•ฉ์ณ ์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
5. fork: ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰์‹œ์ผœ์ฃผ๋Š” ์ดํŽ™ํŠธ, ํ•˜์ง€๋งŒ ๋น„๋™๊ธฐ ์‹คํ–‰์„ ํ•œ๋‹ค.
6. call: promise๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ๊ฒฐ๊ณผ๊ฐ€ ๋ฐ˜ํ™˜ ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฐ๋‹ค(์ฆ‰, ๋™๊ธฐ๋กœ ์‹คํ–‰)
```


### ๐Ÿ” Redux, Redux-Saga Setting
```javascript
1. Connected React Router
- ๋ฆฌ๋•์Šค์—์„œ history ๊ฐ์ฒด ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
- ์ฃผ์˜์‚ฌํ•ญ router ๋ฆฌ๋“€์„œ๋ช…์€ router๋กœ ๊ณ ์ •!
- history๋Š” history ๋ชจ๋“ˆ์—์„œ createBrowserHistory๋กœ ๋ฐ›์•„์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
- ์ฐธ๊ณ : https://hokeydokey.tistory.com/m/74?category=783109

2. createSagaMiddleware
- redux-saga ๋ฏธ๋“ค์›จ์–ด๋ฅผ Redux Subspace ๋ฏธ๋“ค์›จ์–ด๋กœ ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ
- const sagaMiddleware = createSagaMiddleware();

3. redux์˜ store ์ƒ์„ฑ, ๋ฆฌ๋“€์„œ์™€ ๋ฏธ๋“ค์›จ์–ด ์‚ฌ์šฉ
- const store = createStore(reducers, applyMiddleware(sagaMiddleware)

4. ํ•ญ์ƒ store ๋ณด๋‹ค ์•„๋ž˜์—์„œ ์ฝ”๋“œ๊ฐ€ ์ž‘์„ฑ๋˜์–ด์•ผ ํ•œ๋‹ค. rootSaga๋ฅผ ์ธ์ž๋กœ ๋‘”๋‹ค.
- sagaMiddleware.run(rootSaga)

6.
ReactDOM.render(


,
document.querSelector('#root');
)
```

### ๐Ÿ” 3. Redux-Saga Process
```javascript
1. ๐Ÿ“ƒ ์•ก์…˜ ํƒ€์ž… ์ •์˜ ์˜ˆ์‹œ
export const LOGIN_REQUEST = "LOGIN_REQUEST";
export const LOGIN_SUCCESS = "LOGIN_SUCCESS";
export const LOGIN_FAILURE = "LOGIN_FAILURE";

2. ๐Ÿ“ƒ ์ดˆ๊ธฐ ๊ฐ’ ๋ฐ Reducer ํ•จ์ˆ˜ ์ž‘์„ฑ
const initialState = {
token: localStorage.getItem('token'),
isAuthenticated: null,
isLoading: false,
...
}

const authReducer = (state = initialState, action) => {
switch (action.type) {
case LOGIN_REQUEST:
return {
...state,
errorMsg: "",
isLoading: true,
}
default:
return state
}

export default authReducer;

3. ๐Ÿ“ƒ rootReducer์— ํ†ตํ•ฉ
import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
import authReducer from './authReducer';
import postReducer from './PostReducer';
import commentReducer from './commentReducer';

const createRootReducer = (history) => combineReducers({
router: connectRouter(history),
auth: authReducer,
post: postReducer,
comment: commentReducer,
})

export default createRootReducer;

4. ๐Ÿ“ƒ Saga ์ž‘์„ฑ
function* logout(action) {
try {
yield put({
type: LOGOUT_SUCCESS,
});
} catch (e) {
yield put({
type: LOGOUT_FAILURE,
});
console.log(e);
}
}

function* watchLogout() {
yield takeEvery(LOGOUT_REQUEST, logout);
}
//authSaga() ์—ฌ๋Ÿฌ Saga ํ†ตํ•ฉ
export default function* authSaga() {
yield all([
fork(watchLoginUser),
fork(watchLogout),
fork(watchLoadingUser),
]);
}

5. ๐Ÿ“ƒ rootSaga์— ํ†ตํ•ฉ
import { all, fork } from 'redux-saga/effects';
import postSaga from './postSaga';
import authSaga from './authSaga';
import commentSaga from './commentSaga';

export default function* rootSaga() {
yield all([
fork(authSaga),
fork(postSaga),
fork(commentSaga),
]);
}

6. ๐Ÿ“ƒ dispatch๋กœ ์•ก์…˜ ๋ฐœ์ƒ
const handleToggle = () => {
dispatch({
type: CLEAR_ERROR_REQUEST,
})
setModal(!modal);
};
```


## ๐Ÿƒ useParams()
- react-router-dom์˜ useParams Hook์„ ์‚ฌ์šฉํ•˜๋ฉด ํ•˜๋‚˜์˜ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ „๋‹ฌ๋ฐ›์€ URL Parameter๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
```js
import { useParams } from 'react-router';

let { categoryName } = useParams();
```


- useParams()๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋”๋ผ๋„ `match` ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด์„œ๋„ URL Parameter๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๋‹ค.
```js
const categoryName = props.match.params.categoryName;
```


## ๐Ÿƒ Infinite Scroll
- IntersectionObserver ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์„œ Infinite Scroll์„ ๊ตฌํ˜„
- useOnScreen์ด๋ผ๋Š” Custom hooks ๊ตฌํ˜„
```js
const skipNumberRef = useRef(0);
const postCountRef = useRef(0);
const endMsg = useRef(false);

postCountRef.current = postCount - 6;

const useOnScreen = (options) => {
const lastPostElementRef = useRef();

useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
setVisible(entry.isIntersecting); //true or false

if (entry.isIntersecting) {
let remainPostCount = postCountRef.current - skipNumberRef.current;

if (remainPostCount >= 0) {
dispatch({
type: POSTS_LOADING_REQUEST,
payload: skipNumberRef.current + 6,
});
skipNumberRef.current += 6;
} else {
endMsg.current = true;
console.log(endMsg.current);
}
}
}, options);

if (lastPostElementRef.current) {
observer.observe(lastPostElementRef.current);
}

const LastElementReturnFunc = () => {
if (lastPostElementRef.current) {
observer.unobserve(lastPostElementRef.current);
}
};

return LastElementReturnFunc;
}, [lastPostElementRef, options]);

return lastPostElementRef;
};

const lastPostElementRef = useOnScreen({
threshold: "0.5",
});
```

```js
return (
<>
...

{loading && GrowingSpinner}

{loading ? (
""
) : endMsg ? (


๋” ์ด์ƒ์˜ ํฌ์ŠคํŠธ๋Š” ์—†์Šต๋‹ˆ๋‹ค.


) : (
""
)}
>
)
```


## ๐Ÿƒ console.log ์ œ๊ฑฐ
- ํŒจํ‚ค์ง€ ๋‹ค์šด๋กœ๋“œ
```
npm i babel-plugin-transform-remove-console
```


- webpack.config.js์—์„œ loaderMap ๋ฐ‘์— `["transform-remove-console", { exclude: ["error", "warn"] }]`์ถ”๊ฐ€
```js
plugins: [
[
require.resolve("babel-plugin-named-asset-import"),
{
loaderMap: {
svg: {
ReactComponent:
"@svgr/webpack?-svgo,+titleProp,+ref![path]",
},
},
},
],
["transform-remove-console", { exclude: ["error", "warn"] }],
isEnvDevelopment &&
shouldUseReactRefresh &&
require.resolve("react-refresh/babel"),
].filter(Boolean),
```