본문 바로가기
개발 이야기/React

React 메뉴 활성/비활성 상태 관리하기

by 농개 2020. 2. 5.
반응형

대부분의 웹사이트는 현재 표시하는 메뉴가 어떤 메뉴인지 사용자에게 알려줍니다. 색깔을 다르게 표시한다던가 해서 말이죠.

이번에는 React로 Menu에 대한 상태 관리 방법을 간단하게 정리해봅니다.

상태관리 도구는 Redux를 사용합니다. 아래 예제는 타입스크립트를 사용했습니다.

 

 

01. index.ts / App.ts

// index.ts
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import store from "./store/configureStore";

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>, document.getElementById('root'));
    
    
    
// App.ts
import React from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import { history } from './store/configureStore';
import Base from './containers/Base';

const App: React.FC = () => {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/home" component={() => <div>홈</div>}></Route>
        <Route path="/info" component={() => <div>정보</div>}></Route>
        <Route path="/contact" component={() => <div>연락처</div>}></Route>
        <Route component={NotFound}></Route>
      </Switch>
      <Base />
    </Router>
  );
}

index.ts는 redux store를 사용하기 위해 App을 Provider로 감싸줬구요.(일반적이지요.)

App.ts에서 Router를 정의했습니다.

그리고 중요한 것!!

바로 Base라는 컴포넌트 입니다.

Base는 웹페이지 호출 마다 실행 시키는 공통(?)모듈이라 보시면 될듯합니다.

메뉴의 상태관리 또한 Common한 성격을 띄기 때문에 해당 컴포넌트에서 담당할 예정입니다.

 

Base 컴포넌트는 나중에 구현할 꺼구요. 먼저 actionreducer를 정의 해봅시다.

 

 

02. Action / Reducer 작성

// 액션 (./store/actions/commonActions.ts)
export const CHANGE_ACTIVE_MENU = 'CHANGE_ACTIVE_MENU' as const;

export function changeActiveMenu(payload: string){
  return {
    type: CHANGE_ACTIVE_MENU,
    payload
  }
}


// 리듀서 (./store/reducers/commonReducer.ts)
import * as common from '../actions/commonActions'
import { produce } from 'immer';  // 상태관리 불변성을 쉽게 해주는 라이브러리.

interface ActionType {
  type: string;
  payload: string;
}

interface Menu {
  name: string;
  path: string;
  isActive: boolean;
}

const initialState = {
  menus: [
    {
      name: '홈',
      path: '/home',
      isActive: false
    },
    {
      name: '정보',
      path: '/info',
      isActive: false
    },
    {
      name: '연락처',
      path: '/contact',
      isActive: false
    },
  ]
}

export const reducer = (state = initialState, action: ActionType) => {
  switch (action.type) {
    case common.CHANGE_ACTIVE_MENU:
      // 현재 url은 활성화, 나머지는 비활성화
      return produce(state, draft => {
        draft.menus.forEach((menu: Menu) => {
          const nowUrl = action.payload;

          if(nowUrl === menu.path) {
            menu.isActive = true;
          } else{
            menu.isActive = false;
          }
        })
      })
    default:
      return state;
  }
}

menus라는 필드명으로 상태관리를 할겁니다.

그리고 immer(상태관리 라이브러리)를 써서 좀 더 쉽게 불변성을 다뤘습니다.(state 요소 중 object나 array 타입이 있으면 걍 무조건 쓰세요...)

CHANGE_ACTIVE_MENU 이벤트가 발생하면, menus 중 payload로 넘어온 url만 active 시킴니다.

payload는 당연히 현재 url을 넘기게 만들겁니다.

 

 

 

03. Base 컴포넌트 작성(중요)

import React, { Component } from 'react'
import { connect } from 'react-redux';
import * as common from '../../store/actions/commonActions';
import { withRouter } from 'react-router-dom';

interface Props {
  changeActiveMenu: (payload: string) => void;
  location: { pathname: string };    // props로 부터 location.pathname을 사용해 현재 url을 얻음
}

class Base extends Component<Props> {
  componentDidMount() {
  	// 최초 마운트 시
    this.props.changeActiveMenu(this.props.location.pathname);
  }

  componentDidUpdate(prevProps: any) {
    // 컴포넌트가 업데이트 될 때 
    if (this.props.location !== prevProps.location) {
      // URL이 변경되었을 때
      this.props.changeActiveMenu(this.props.location.pathname);
    }
  }

  render() {
  	// Base는 단순히 Common 한 기능들을 수행하는 컴포넌트라서 따로 rendering 할건 없습니다
    return (
      <div></div>
    )
  }
}

// 반드시 withRouter로 감싸줘야함!!
export default withRouter(connect(
  null,
  (dispatch) => {
    return {
      changeActiveMenu: (payload: string) => {
        dispatch({ type: common.CHANGE_ACTIVE_MENU, payload: payload})
      }
    }
  }
)(Base))

 

위와 같이 껍데기(?) Base 컴포넌트를 작성해 줬습니다.

이때 반드시 withRouter를 사용해서 해당 컴포넌트가 렌더링 될 때 props에서 location을 얻어올수 있도록 해야합니다.

location.pathname이 현재 URL입니다.( http://localhost:3000/home 이라면 "/home"이 출력됩니다.)

 

 

04. MenuBar 컴포넌트 작성

import React, { Component } from 'react'
import { history } from '../../store/configureStore';
import { connect } from 'react-redux';

interface Props {
  menus: Array<Object>;
}

interface Menu { path: string; name: string, isActive: boolean };

class MenuBar extends Component<Props>{

  handleClickMenu = (url: string) => {
    history.push(url);  // URL 이동
  }

  render() {
    const { menus } = this.props;
    return (
      <ul>
        {
          props.menus.map((obj: Menu, idx: number) => {
            return (
              <li key={idx} onClick={() => this.handleClickMenu(obj.path)} className={obj.isActive?"active":""}>
                {obj.name}
              </li>
            )
          })
        }
      </ul>
    )
  }
}

export default connect(
  (state: any) => {
    return {
      menus: state.common.menus
    }
  },
  null
)(MenuBar);

menus를 redux store로 부터 가져와 MenuBar 컴포넌트에서 사용합니다.

메뉴를 클릭하면 hitstory.push를 이용해 페이지를 변경합니다.

그리고 isActive가 true인 것만 active라는 이름의 class를 추가하여 style을 적용시키면 됩니다.

 

 

 

 

반응형