React/게시판만들기 v2.

[React][CRUD] 게시판 만들기 All in One (2). npm install, view, router 생성

binaryJournalist 2021. 4. 14. 13:10
반응형

 

 

목차돌아가기:

binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2

 

[React][CRUD] create-board-tutorial-v2

* 인용시 출처 부탁드립니다. 완성 소스 code: github.com/cruellaDev/react-create-board-v2 cruellaDev/react-create-board-v2 updated version of react-create-board. Contribute to cruellaDev/react-create-..

binaryjourney.tistory.com

 

 

 

앱을 키기 전에 일단 모듈을 설치하겠다.

 

npm i이나 yarn add 도 괜춘하다

 

모듈이 설치될 폴더위치는 ./reat-create-board 이다.

 

yarn add redux react-redux react-router react-router-dom redux-saga @reduxjs/toolkit axios redux-logger ramda query-string

 

ramda 는 자주쓰는 ramda 함수 형태를 가지고 있는 모듈 같다.

 

나는 object를 배열화해서 사용해주는 함수를 사용했다. (한번밖에 사용 안함)

 

 

react-bootstrap 은 나중에 필요하게 되면 그 떄 설치하겠다.

 

설치 후 package.json의 모습

 

{
  "name": "react-create-board",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@reduxjs/toolkit": "^1.5.1",
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    "axios": "^0.21.1",
    "json-server": "^0.16.3",
    "query-string": "^7.0.0",
    "ramda": "^0.27.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2",
    "react-redux": "^7.2.3",
    "react-router": "^5.2.0",
    "react-router-dom": "^5.2.0",
    "react-scripts": "4.0.3",
    "redux": "^4.0.5",
    "redux-logger": "^3.0.6",
    "redux-saga": "^1.1.3",
    "web-vitals": "^1.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server ../server/board.json --watch --port 4000"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

 

여기까지 잘 와야 한다.

 

 

 

/**

* 주의

* 2022-03-24

* 이 글을 작성한 시점은 2021년 4월으로 약 1년 전이고 그 사이에 많은 모듈들이 수차례 업그레이드를 진행하였기에

* 위 패키지 그대로 할 시 컴파일이 안될 수도 있다.

* Node 버전과 react-router 버전 확인 후 진행 요망

* (npx create-react-app 의 경우 Node 버전이 낮을 경우 안 될 수 있다. 업그레이드 해야 함)

* react-router 의 경우도 v5에서 v6과 차이점이 있다.

*/

참고2 - react-router) https://velog.io/@kcdoggo/Switch-is-not-exported-from-react-router-dom-%EC%97%90%EB%9F%AC

 

'Switch' is not exported from 'react-router-dom' 에러

react-router-dom이 버전 6로 업그레이드되면서, Switch를 더이상 지원을 안하게 됬다. Switch -> routes로 바뀜. 또한 component도 element로 바꼈다.공식문서 참조해서 코드를 조금 바꾸면 된다.

velog.io

참고1 - react-router) https://reactrouter.com/docs/en/v6/upgrading/v5

 

React Router | Upgrading from v5

Declarative routing for React apps at any scale

reactrouter.com

 

 

 

만약 cannot resolve  babel 어쩌구가 나온다면 터미널을 kill 하고 다시 켜보길 바란다.

 

 

 

일단 구성해 놓은 디렉토리 안의 파일들을 다시 보자.

 

 

 

 

 

화면정의서 겸 스토리보드를 그려 봤는데 계획은 이러하다.

 

(이 블로그를 작성했던 4월 초만 해도 저 그림대로 갈 계획이었는데 현재 설정 기능이 조금 추가됐다.)

 

 

 

이 계획에 맞춰 router 와 view 를 만들어보자.

 

 

 

 

우선 /src/views 디렉토리 내 파일들은 rfce 단축키만 이용하여 빈 껍데기만 만든다.

 

 

// Main.js

import React from 'react';

function Main() {
    return (
        <>
            <div>Welcome!</div>
        </>
    );
}

export default Main;

 

// Board.js

import React from 'react'

function Board() {
    return (
        <div>
            보드메뉴
        </div>
    )
}

export default Board

 

// Article.js

import React from 'react'

function Article() {
    return (
        <div>
            게시글 상세
        </div>
    )
}

export default Article

 

// ArticleList.js

import React from 'react'

function ArticleList() {
    return (
        <div>
            게시글 목록
        </div>
    )
}

export default ArticleList

 

// Comment.js

import React from 'react'

function Comments() {
    return (
        <div>
            댓글
        </div>
    )
}

export default Comments

 

// Control.js

import React from 'react'

function Control() {
    return (
        <div>
            설정
        </div>
    )
}

export default Control

 

// Post.js

import React from 'react'

function Post() {
    return (
        <div>
            글쓰기
        </div>
    )
}

export default Post

 

 

여기까지 완료되면 이제 index.js 에 Router를 import 하여 App 컴포넌트를 감싸줄 차례인데

 

어차피 redux 로 state 제어를 하려면 store를 생성해서 index에 넣어줘야 하니

 

index.js 를 한꺼번에 편집해보도록 한다.

 

 

우선

 

/src/utils 폴더에

history.js

store.js 를 만든다.

 

// utils/history.js

import { createBrowserHistory } from "history";

export default createBrowserHistory();

 

 

아 store 를 다 만들어주기 전에 빼먹은 게 있다.

 

 

rootReducer와 rootSaga를 만들어줘야 한다.

 

 

모두 기본 src 폴더 안에 바로 파일을 생성해준다.

 

// rootSaga.js

import { map } from 'ramda';
import { all, fork  } from "redux-saga/effects"

let combineSagas = {};

export default function* rootSaga() {
    yield all(map(fork, combineSagas));
}

 

일단 rootSaga는 이렇게만 만들어주고

 

 

// rootReducer.js

import { combineReducers } from 'redux';

const rootReducer = combineReducers({  });

export default rootReducer;

 

rootReducer는 이렇게만 만들어준다.

 

 

store.js 에 두 파일 모두 import가 필요하다.

 

// store.js

import { configureStore } from "@reduxjs/toolkit";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import rootReducer from "../rootReducer";
import rootSaga from "../rootSaga";
import history from "./history";

const sagaMiddleware = createSagaMiddleware({
    context: { history: history },
});
const initialState = {};

const store = configureStore({
    reducer: rootReducer,
    middleware: [sagaMiddleware, logger],
    devTools: true,
    preloadedState: initialState,
});

sagaMiddleware.run(rootSaga);

export default store;

 

 

(혹시라도 여기서 compile 에러가 난다면 댓글을 남겨주세요..!)

 

 

보통은 redux의 createStore 를 이용하나 createStore는 기본적으로 middleware를 thunk를 사용하기 때문에 thunk를 사용하지 않는다면 다른 미들웨어로 덮어씌워줘야 한다.

 

결국 configure가 필요하단 의미인데

redux에서는 store를 생성하면서 configure까지 해주는 configureStore 메서드를 제공해주고 있다.

 

그래서 사용하였다.

 

createSagaMiddleware 에서 넣어주는 context는 initialState 같은 개념으로 보면 된다.

 

christopher 님의 도움을 받아 추가로 더 알게 된 것인데

같이 추측해 본 바로는 context API 를 본따 만든 개념이 아닐까 싶었다.

원하는 정보를 미리 저장해놓고 getContext("키이름") 을 사용하여 꺼내 쓸 수 있다.

해당 미들웨어에서 setContext로 다시 내장할 수도 있다.

 

그래서 saga에서 화면전환을 위해 사용할 history를 context에 넣어주는 것인데

getContext를 사용할 경우 context 에 history를 넣고 그게 아니라면 생략해도 된다. (saga에서 history import 하여 사용 가능함)

 

나는 getContext 사용방법을 나중에 설명할 것이므로 일단 넣기로 하겠다.

 

 

 

 

index.js 에서는 사용할 store 와 history를 import 해준다.

 

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Router } from 'react-router-dom';
// import { BrowserRouter } from 'react-router-dom'; // 나중에 비교해보세요.
/**
 * https://stackoverflow.com/questions/56707885/browserrouter-vs-router-with-history-push
 * 
 * BrowserRouter ignores the history prop as it handles the history automatically for you.
 * If you need access to the history outside of a react component, then using Router should be fine.
 */
import App from './App';
import { Provider } from "react-redux";
import store from "./utils/store";
import history from "./utils/history";

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <App />
    </Router>
  </Provider>,
  document.getElementById('root')
);

 

게시판 만들기 v1의 경우 react-router 의 Router를 사용해주었는데 그 때는 react-router-dom 에도 Router가 있는지 모르고 사용했었다.

웹 개발용에 적합한 react-router-dom 을 사용하는 게 성능상 더 좋다고들 한다.

 

 

보통은 BrowerRouter 를 Router로 import 하여 App을 감싸기 때문에 history도 필요없지만

 

나같은 경우 redux-saga라는 외부 라이브러리에서도 history 에 접근하기 위해 BrowserRouter가 아닌 Router를 사용하고 그 때문에 history 를 Router의 props를 따로 넣어줘야 한다.

 

 

stackoverflow.com/questions/56707885/browserrouter-vs-router-with-history-push

 

BrowserRouter vs Router with history.push()

I am trying to understand the difference between BrowserRouter and Router of the react-router-dom (v5) package and what difference it makes for my example below. The documentation says: Browser...

stackoverflow.com

 

위는 참고가 된 글이다.

 

 

 

어떤 Router와 BrowserRouter의 차이를 아직 모르겠다면 나중에 saga 에서 history.push를 쓰게 되었을 때

한 번 index.js 에서 BrowserRouter 부분의 주석을 풀고 화면전환이 되는지 실험해보길 바란다.

 

 

 

 

 

 

그리고 url에 따라 다른 컴포넌트를 렌더링해줄 /src/routes/Routes.js 를 만들어준다.

 

 

 

/src/routes/Routes.js 는 url 주소를 바꿔주는 기능을 할 건데 다음과 같이 만든다

 

// Routes.js

import React, { Suspense, lazy } from 'react';
import { Route, Switch} from "react-router-dom";

function Routes() {
    const Main = lazy(() => import('../views/Main'));
    const ArticleList = lazy(() => import('../views/ArticleList'));
    const Article = lazy(() => import('../views/Article'));
    const Post = lazy(() => import('../views/Post'));
    const Control = lazy(() => import('../views/Control'));

    return (
        <div>
            <Suspense fallback={<div>Loading...</div>}>
                <Switch>
                    <Route path={"/"} exact component={Main} />
                    <Route path={"/board/:boardId"} exact component={ArticleList} />
                    <Route path={"/article/:articleId"} exact component={Article} />
                    <Route path={"/insert"} exact component={Post} />
                    <Route path={"/update/:articleId"} exact component={Post} />
                    <Route path={"/control"} exact component={Control} />
                    <Route path={"*"} component={Main} />
                </Switch>
            </Suspense>
        </div>
    );
}

export default Routes;

 

Suspense 는 리액트에서 아직 안정된 배포판에서는 제공되지 않은 실험적인 기능이라고 하는데

 

나는 어차피 혼자 만들기용이니까 사용하겠다.

 

생각보다 되게 좋은 기능이다. 정식 배포되면 좋겠음

 

 

 

쓰기 싫은 사람들은 Suspense 컴포넌트와

const Main = lazy(() => import('../views/Main')); 이런 형식을 코드들을 지우고

 

 

import Main from '../views/Main';

 

이런 식으로 import 해서 쓰면 된다.

 

 

ko.reactjs.org/docs/concurrent-mode-suspense.html

 

데이터를 가져오기 위한 Suspense (실험 단계) – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

 

코드에서 보이는 lazy 는 지연로딩으로 JPA의 FetchType.LAZY 와 유사한 기능을 한다.

 

 

그리고 위 사이트의 설명을 보면

 

Suspense는 데이터 불러오기 라이브러리가 아닙니다. Suspense는 컴포넌트가 읽어들이고 있는 데이터가 아직 준비되지 않았다고 React에 알려줄 수 있는, 데이터 불러오기 라이브러리에서 사용할 수 있는 메커니즘입니다. 이후에 React는 데이터가 준비되기를 기다렸다가 UI를 갱신할 수 있습니다. Facebook에서는 Relay와 Relay가 제공하는 새로운 Suspense 통합 기능을 사용하고 있습니다. Apollo와 같은 다른 라이브러리에서도 유사한 통합 기능을 제공할 것으로 기대합니다.

 

 

말로 설명하긴 어렵지만 뭔가 pending 작용을 해주는 것 같다.

 

 

<Route path={"/"} exact component={Main} />
<Route path={"/board/:boardId"} exact component={ArticleList} />
<Route path={"/article/:articleId"} exact component={Article} />
<Route path={"/insert"} exact component={Post} />
<Route path={"/update/:articleId"} exact component={Post} />
<Route path={"/control"} exact component={Control} />
<Route path={"*"} component={Main} />

 

코드를 더 뜯어보면

/article/:articleId 는 :뒤로 숫자가 들어올 것인데 이는 parameter 로 articleId 라는 키의 값으로 parameter 숫자를 받는다는 것이다.

 

 

/article/articleId(숫자) 일 경우 게시글 내용 페이지로 이동하고

/insert 혹은 update/articleId 일 경우 해당 게시글 id를 가진 게시글을 수정하는 페이지로 이동한다.

 

그외 나머지 이상한 주소들은 메인페이지로 이동한다. (path={"*"})

 

 

이제 /src/Views.js 에서 sidebar 역할을 할 Board와 주소마다 다른 컴포넌트를 렌더링 해줄 Routes 를 묶어주자.

 

 

 

// Views.js

import React from 'react';
import Board from "./views/Board";
import Routes from "./routes/Routes";

function Views() {
    return (
        <div >
            <div id="header" className="header">
                <div >
                    <h3>Board CRUD</h3>
                </div>
            </div>
            <div id="sidebar" className="sidebar">
                <Board />
            </div>
            <div id="content" className="content">
                <Routes />
            </div>
        </div>
    );
}

export default Views;

 

코드를 보면 className 이 보일 것이다.

 

 

이건 이제 /src/Views.css 를 만들면서 새로 생길 className 들이다.

 

.header {
    display: block;
    height: 200px;
    width: 100%;
}

.sidebar {
    display: inline-block;
    height: calc(100% - 200px);
    width: 200px;
}

.content {
    display: inline-block;
    height: calc(100% - 200px);
    width: calc(100% - 200px);
}

.hide {
    display: none;
} 

 

myurm 님한테서 style 도움을 받았다.

 

 

css 파일을 만들고

 

 이제 Views 에

 

import "./Views.css";

 

한 줄 추가해준다.

 

 

 

 

마지막으로 App에 Views를 import 하면된다.

// App.js

import Views from "./Views";

function App() {
    return (
        <div>
            <Views />
        </div>
    );
}

export default App;

 

 

이제

 

http://localhost:3000/

 

http://localhost:3000/article/1

 

 

이 주소 어디를 치든 Board(보드메뉴로 보임)가 따라올 것이다.

 

 

 

 

voila!

 

 

포스팅 작성하면서 에러를 잡기 위해 수정한 부분이 많으니 혹시 달라졌거나 파일이 빠졌거나 하는 부분은 댓글로 달아주길 바란다.

 

 

 

목차돌아가기:

binaryjourney.tistory.com/pages/ReactCRUD-create-board-tutorial-v2

 

[React][CRUD] create-board-tutorial-v2

* 인용시 출처 부탁드립니다. 완성 소스 code: github.com/cruellaDev/react-create-board-v2 cruellaDev/react-create-board-v2 updated version of react-create-board. Contribute to cruellaDev/react-create-..

binaryjourney.tistory.com

 

반응형