React 라우팅, 리덕스

이선민·2021년 7월 1일
0

라우팅

SPA (Single Page Application)

서버에서 주는 html이 1개 뿐이 어플리케이션

html을 하나만 주는 이유?

→ 제일 중요한 건 사용성! 때문이다.

페이지를 이동할 때마다 서버에서 주는 html로 화면을 바꾸다보면 상태 유지가 어렵고,
바뀌지 않은 부분까지 새로 불러오기 때문에 비효율적이다.
(사용자가 회원가입하다가 적었던 내용이 날아갈 수 도 있다)

html을 하나만 주는 이유? → 단점은?

SPA는 딱 한 번 정적자원을 내려받다보니, 처음에 모든 컴포넌트를 받아온다.
즉, 사용자가 안들어가 볼 페이지까지 전부 한 번에 가지고 오기 때문에 많은 컴포넌트가 
있다면 첫 로딩 속도가 느려진다.

SPA는 주소를 어떻게 옮길 수 있을까?

html은 딱 하나를 가지고 있지만, SPA도 브라우저 주소창대로 다른 페이지를 보여줄 수 있다.
이렇게 브라우저 주소에 따라 다른 페이지를 보여주는 걸 라우팅이라고 부른다.

라우팅 처리하기

react-route-dom 패키지 설치

yarn add react-router-dom

공식 문서 → https://reactrouter.com/web/guides/primary-components

페이지 전환

1. index.js에 BrowserRouter 적용하기
import { BrowserRouter } from "react-router-dom";

// index.html에 있는 div#root에 우리가 만든 컴포넌트를 실제로 랜더링하도록
// 연결해주는 부분
ReactDOM.render(
  <BorwserRouter>
  <App />
  </BorwserRouter>,
  document.getElementById("root")
);
BrowserRouter는 웹 브라우저가 가지고 있는 주소 관련 정보를 props로 넘겨준다.
현재 내가 어느 주소를 보고 있는 지 쉽게 알 수 있게 도와줌


2. 세부 화면 만들기
(Home.js, Cat.js, Dog.js)
import React from "react";
   // Cat, Dog
const Home = (props) => {
 
  return ( // 고양이 화면, 강아지 화면
  	<div>메인 화면</div>
  );
}

export default Home; // Cat, Dog
// Cat.js, Dog.js 도 위와 같이 동일
3. App.js에서 Route 적용하기
방법 1 넘겨줄 props가 없을 때
→ <Route path="/home" component={ Home }>
path="주소[/home 처럼 /와 주소를 적는다]" component={[보여줄 컴포넌트]}

방법 2 넘겨줄 props가 있을 때
<Route path="주소[/home처럼 /와 주소를 적는다]" render={(props) => (<BucketList list={this.state.list} />)} />
4. exact 적용하기
화면을 확인해보면 /cat 과 /dog에서 Home 컴포넌트도 같이 나온다
→ "/"이 기호가 "/cat"과 "/dog"에도 포함되어 있기 때문
// 해결 방법
<Route path="/" exact component={Home} />
<Route path="/cat" component={Cat} />
<Route path="/dog" component={Dog} />
5. URL 파라미터사용하기
생김새 → /cat/nabi
// 파라미터 주는 방법
// App.js
...
// 파라미터 주기
 <Route path="/cat/:cat_name" component={Cat} />


// 파라미터 사용 방법
// Cat.js
import React from "react";

const Cat = (props) => {
  console.log(props.match);
  
  return(
  	<div>고양이 화면</div>
  );
}

export default Cat;
/cat/nabi로 주소를 이동해서 콘솔에 파라미터가 어떻게 찍히는지 확인

6. 링크 이동 시키기
<Link/> 사용 방법
링크 컴포넌트는 html 중 a 태그와 비슷한 역할을 한다. 리액트 내에서 페이지 전환을 도와줌
<Link to="주소">텍스트</Link>
<Link to="/">Home</Link>
<Link to="/cat">Cat</Link>
<Link to="/dog">Dog</Link>

라우팅, 조금 더 꼼꼼히 쓰기

잘못된 주소 처리하기

exact → 중복 주소를 처리하는 방법

미리 정하지 않은 주소로 들어온 경우 다뤄보기
1. NotFound.js 파일을 만들고 빈 컴포넌트 만들기
import React from "react";

const NotFound = (props) => {
  return <h1>주소가 올바르지 않습니다.</h1>
};

export default NotFound;
App.js에서 불러오기
import NotFound from "./NotFound";
Switch 추가하고 NotFound 컴포넌트를 Route에 주소 없이 연결하면 끝
<Switch>
  <Route component={NotFound} />
<Switch>

리덕스

리덕스란?

리덕스는 아주 흔히 사용하는 상태관리 라이브러리
전역 상태관리를 편히 할 수 있게 해준다.

리덕스는 데이터를 한 군데 몰아넣고, 여기저기에서 꺼내볼 수 있게 해준다.

리덕스 패키지 설치하기
yarn add redux react-redux

리덕스의 기본 용어

State → 리덕스에서는 저장하고 있는 상태값("데이터"라고 생각하기)을 state라고 부른다.
딕셔너리 형태({[key]: value})형태로 보관


Action → 상태에 변화가 필요할 때(=가지고 있는 데이터를 변경할 때) 발생하는 것
액션은 객체이다. type은 이름, 임의의 문자열을 넣는다
{type: 'CHANGE_STATE', data: {...}}


ActionCreator → 액션 생성 함수, 액션을 만들기 위해 사용한다.
// 이름 그대로 함수!
const changeState = (new_data) => {
  // 액션을 리턴
  return {
    type: 'CHANGE_STATEl',
    data: new_data
  }
}
Reducer → 리덕스에 저장된 상태(=데이터)를 변경하는 함수
액션 생성 함수를 부르고 → 액션을 만들면 → 리듀서가 현재 상태(=데이터)와 액션 객체를 받아서
→ 새로운 데이터를 만들고 → 리턴해준다.
// 기본 상태값을 임의로 정함
const initialState = {
  name: 'test1'
}

function reducer(state = initialState, action) {
  switch(action.type) {
      
      // action의 타입마다 케이스문을 걸어주면,
      // 액션에 따라서 새로운 값을 돌려준다
    case CHANGE_STATE:
      return {name: 'test2'};
      
    default:
      return false;
  }
}
Store → 프로젝트에 리덕스를 적용하기 위해 만드는 것!
스토어(우리가 데이터를 볼 수 있게 해줌)에는 리듀서, 현재 애플리케이션 상태, 
리덕스에서 값을 가져오고 액션을 호출하기 위한 몇 가지 내장 함수가 포함되어 있다.
생김새는 딕셔너리 혹은 json처럼 생겼다.
내장함수 → 공식문서에서 보기


dispatch → 디스패치는 많이 사용되는 스토어의 내장 함수이다.
액션을 발생 시키는 역할을 한다.
실제로는 코드가 길지만 간단하게 표현하자면 → dispatch(action)
이런 식으로 우리가 발생시키고자 하는 액션을 파라미터로 넘겨서 사용한다.

리덕스의 3가지 특징

1. store는 1개만 쓴다.
리덕스는 단일 스토어 규칙을 따른다. 한 프로젝트에 스토어는 하나만 쓰기!


2. store의 state(데이터)는 오직 action으로만 변경할 수 있다.
리액트에서도 state는 setState()나, useState() 훅을 써서만 변경 가능했죠! 
데이터가 마구잡이로 변하지 않도록 **불변성**을 유지해주기 위함입니다.
불변성 뭐냐구요? 간단해요! 허락없이 데이터가 바뀌면 안된단 소리입니다!

조금 더 그럴 듯하게 말하면, 리덕스에 저장된 데이터 = 상태 = state는 **읽기 전용**입니다.

**그런데... 액션으로 변경을 일으킨다면서요? 리듀서에서 변한다고 했잖아요?**
→ 네, 그것도 맞아요. 조금 더 정확히 해볼까요!
가지고 있던 값을 수정하지 않고, 새로운 값을 만들어서 상태를 갈아끼웁니다!
즉, A에 +1을 할 때, 
A = A+1이 되는 게 아니고, A' = A+1이라고 새로운 값을 만들고  A를 A'로 바꾸죠.


3. 어떤 요청이 와도 리듀서는 같은 동작을 해야한다.
리듀서는 순수한 함수여야 한다!

순수힌 힘수란?
→ 파라미터 외의 값에 의존하지 않아야하고,
→ 이전 상태는 수정하지(=건드리지)않는다.(변화를 준 새로운 객체를 return 해야함)
→ 파라미터가 같으면, 항상 같은 값을 반환
→ 리듀서가 이전 상태와 액션을 파라미터로 받는다.

리덕스를 통한 리액트 상태관리

리덕스는 여러 컴포넌트가 동일한 상태를 보고 있을 때 굉장히 유용하다.
또, 데이터를 관리하는 로직을 컴포넌트에서 빼면 컴포넌트는 정말 뷰만 관리할 수 있다. 코드가 깔끔하면 유지보수에도 좋다.

상태관리 흐름

상태관리 흐름도 
→ Store, Action, Reducer, Component

1. 리덕스 Store를 Component에 연결한다.
2. Component에서 상태 변화가 필요할 때 Action을 부른다.
3. Reducer를 통해서 새로운 상태 값을 만들고,
4. 새 상태값을 Store에 저장한다.
5. Component는 새로운 상태값을 받아온다.(props를 통해 받아오니깐, 다시 랜더링 된다)

리덕스 써보기

덕스 구조

보통 리덕스를 사용할 때는, 모양새대로 action, actionCreator, reducer를 
분리해서 작성(액션은 액션끼리, 액션생성함수는 액션생성함수끼리, 리듀서는 리듀서끼리 작성한다)

덕스 구조는 모양새로 묶는 대신 기능으로 묶어 작성한다. 
(버킷리스트를 예로 들자면, 버킷리스트의 action, actionCreator, reducer를 한 파일에 넣기)

https://github.com/erikras/ducks-modular-redux

모듈 만들기

폴더부터 만든다.
src 폴더 아래에 redux라는 폴더를 만들고, 그 안에 modules라는 폴더를 만든다.
modules 아래에 bucket.js(=ex)라는 파일을 만든다

1. Action 가져오는 것, 생성하는 것 두가지 액션 만들기
const LOAD = 'bucket/LOAD';
const CREATE = 'bucket/CREATE';
2. initialState
초기 상태값을 만들어주기 (기본 값)
const initialState = {
  list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
};
3. Action Creactor
액션 생성 함수를 작성
export const loadBucket = (bucket) => {
    return { type: LOAD, bucket };
}

export const createBucket = (bucket) => {
    return {type: CREATE, bucket};
}
4. Reducer
리듀서를 작성한다
load할 땐, 가지고 있던 기본값을 그대로 뿌려주기
create할 땐, 새로 받아온 값을 가지고 있던 값에 더해서 리턴해주기
export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    // do reducer stuff
    case "bucket/LOAD":
      return state;

    case "bucket/CREATE":
      const new_bucket_list = [...state.list, action.bucket];
      return { list: new_bucket_list };

    default:
      return state;
  }
}
5. Store
redux 폴더 하위에 configStore.js 파일을 만들고 스토어를 만들기
//configStore.js
import { createStore, combineReducers } from "redux";
import bucket from './modules/bucket';
import { createBrowserHistory } from "history";

// 브라우저 히스토리를 만들어줍니다.
export const history = createBrowserHistory();
// root 리듀서를 만들어줍니다.
// 나중에 리듀서를 여러개 만들게 되면 여기에 하나씩 추가해주는 거예요!
const rootReducer = combineReducers({ bucket });

// 스토어를 만듭니다.
const store = createStore(rootReducer);

export default store;

리덕스와 컴포넌트를 연결하기

Store 연결하기
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {BrowserRouter} from 'react-router-dom';

// 우리의 버킷리스트에 리덕스를 주입해줄 프로바이더를 불러옵니다!
import { Provider } from "react-redux";
// 연결할 스토어도 가지고 와요.
import store from "./redux/configStore";
ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </Provider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

컴포넌트에서 리덕스 데이터 사용하기

컴포넌트에서 리덕스 액션 사용하는 법


 클래스형 컴포넌트에서 리덕스 데이터 사용하기
- -1) 리덕스 모듈과 connect 함수를 불러옵니다.
- -2) 상태값을 가져오는 함수와 액션 생성 함수를 부르는 함수를 만들어줍니다.
- -3) connect로 컴포넌트와 스토어를 엮어줍니다.
- -4) 콘솔에 this.props를 찍어봅니다. (스토어에 있는 값이 잘 나왔는지 볼까요!)
- -5) this.state에  있는 list를 지우고 스토어에 있는 값으로 바꿔봅시다!
- -6) setState를 this.props.create로 바꿔봅시다!


함수형 컴포넌트에서 리덕스 데이터 사용하기
리덕스도 리액트처럼 훅이 있어요. 훅을 사용해서 액션 생성 함수를 불러보고, 스토어에 저장된 값도 가져와 볼게요.


useSelector() 적용하기
// 리액트 패키지를 불러옵니다.
import React from "react";
import styled from "styled-components";

// redux hook을 불러옵니다.
import {useDispatch, useSelector} from 'react-redux';

const BucketList = (props) => {
  // 버킷리스트를 리덕스 훅으로 가져오기
  const bucket_list = useSelector(state => state.bucket.list);

  console.log(bucket_list);
  
  return (
    <ListStyle>
      {bucket_list.map((list, index) => {
        return (
          <ItemStyle
            className="list_item"
            key={index}
            onClick={() => {
              props.history.push("/detail");
            }}
          >
            {list}
          </ItemStyle>
        );
      })}
    </ListStyle>
  );
};

const ListStyle = styled.div`
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow-x: hidden;
  overflow-y: auto;
`;

const ItemStyle = styled.div`
  padding: 16px;
  margin: 8px;
  background-color: aliceblue;
`;

export default BucketList;
URL 파라미터를 적용
//App.js
...
					<Switch>
            <Route
              path="/"
              exact
              render={(props) => <BucketList history={props.history} />}
            />
            <Route
              path="/detail/:index"
              render={(props) => <Detail match={props.match} history={props.history} />}
            />
            <Route
              render={(props) => <NotFound history={props.history} />}
            />
          </Switch>
...
//BucketList.js
...
      {bucket_list.map((list, index) => {
        return (
          <ItemStyle
            className="list_item"
            key={index}
            onClick={() => {
              // 배열의 몇번째 항목을 눌렀는 지, url 파라미터로 넘겨줍니다.
              props.history.push("/detail/"+index);
            }}
          >
            {list}
          </ItemStyle>
        );
      })}
    </ListStyle>
  );
};
...
상세페이지에서 버킷리스트 내용을 띄워보자
//Detail.js
// 리액트 패키지를 불러옵니다.
import React from "react";

// redux hook을 불러옵니다.
import { useDispatch, useSelector } from "react-redux";

const Detail = (props) => {
  // 스토어에서 상태값 가져오기
  const bucket_list = useSelector((state) => state.bucket.list);
  // url 파라미터에서 인덱스 가져오기
  let bucket_index = parseInt(props.match.params.index);

  return <h1>{bucket_list[bucket_index]}</h1>;
};

export default Detail;

0개의 댓글