본문 바로가기
React

[React] Flux패턴 (todolist)

by BeomBe 2024. 3. 3.
반응형

오늘은 Flux 패턴에 대해 이야기 해보겠습니다.

React Flux란?

React Flux는 React 애플리케이션에서 데이터 흐름을 관리하기 위한 아키텍처 패턴입니다. 이 패턴은 단방향 데이터 흐름을 중심으로 구성되어 있으며, 애플리케이션의 상태를 예측 가능하고 유지보수하기 쉽게 만들어줍니다.

 

Flux 아키텍처는 다음과 같은 주요 요소로 구성됩니다:

  1. Actions: 애플리케이션에서 발생하는 이벤트 또는 사용자의 상호작용을 나타내는 객체입니다. Actions는 애플리케이션에서 어떤 일이 발생했는지를 명확하게 정의하고, 액션의 타입과 필요한 데이터를 포함합니다.
  2. Dispatcher: 액션을 받아서 등록된 콜백 함수에게 액션을 전달하는 역할을 합니다. Dispatcher는 단 하나만 존재하며, 애플리케이션에서 발생하는 모든 액션을 처리합니다.
  3. Stores: 애플리케이션의 상태를 저장하고, 관리하는 역할을 합니다. Store는 상태를 변경하는 로직을 가지며, Dispatcher로부터 전달된 액션에 따라 상태를 업데이트합니다. Store는 상태 변경이 완료될 때마다 View에게 알려주어 UI를 업데이트합니다.
  4. Views: 사용자에게 보여지는 컴포넌트입니다. Views는 상태를 표시하고, 사용자의 입력에 반응하여 액션을 발생시킵니다. Views는 액션을 보내고, Store의 상태를 구독하여 변경 사항을 감지하고 UI를 업데이트합니다.

Flux 패턴은 단방향 데이터 흐름을 따르기 때문에 애플리케이션의 상태 변화를 추적하기 쉽고, 디버깅과 테스팅이 용이합니다. 또한, 여러 개의 Store를 사용하여 애플리케이션의 상태를 모듈화하고, 각각의 Store가 독립적으로 동작하도록 할 수 있습니다.

 

Flux 패턴은 Facebook에서 개발한 React와 함께 사용하기 위해 처음 도입되었으며, Redux 등의 상태 관리 라이브러리는 Flux 패턴을 기반으로 구현되었습니다. Flux 패턴을 사용하면 React 애플리케이션의 상태 관리를 효율적으로 할 수 있으며, 애플리케이션의 규모가 커질수록 더욱 가치를 발휘합니다.

 

Flux패턴으로 간단히 Todolist를 만들어 보겠습니다.

 

새 리액트 프로젝트를 만들겠습니다.

npx create-react-app flux-pattern

 

잘 동작되는지 확인 해보겠습니다.

npm start         //잘 설치됐는지 확인

 

리눅스 서버에서 동작시켜 보통분들은 localhost:3000으로 접속하셔서 아래처럼 잘 동작되는지 확인하면 됩니다.

 

기본 프로젝트 생성시에 Flux는 package에 포함되지 않기때문에 설치를 해줍니다.

npm install flux

 

그리고 사용할 패키지들도 함께 설치하겠습니다.

npm install bootstrap

npm install react-router-dom @types/react-router-dom

 

구조는 간단히 아래처럼 했습니다. 자연스럽게 변경해주시면 됩니다.

 

Flux패턴이 코드들을 ActionTypes.jsx -> Actions.jsx -> Dispatcher.jsx -> Store.jsx -> Post.jsx(page) + PostList.jsx 순서로 보여드리면,

//ActionType.jsx - Action Type 지정
const actionType = {
    GET_POSTS: "GET_POSTS",
};

export default actionType;

 

액션은 View에서도 다시 일어날수있다는점을 기억해두시면 됩니다.

따라서 Actions.jsx에도 dispatcher.dispatch를 활용하여 등록된 모든 콜백에 페이로드를 전달합니다.

//Actions.jsx
import dispatcher from "./Dispatcher";
import actionTypes from "./ActionTypes";
import data from "../db.json";

export function getPosts() {
    dispatcher.dispatch({
        actionTypes: actionTypes.GET_POSTS,
        posts: data["posts"],
    });
}

 

//Dispatcher.jsx

import { Dispatcher } from "flux";

const dispatcher = new Dispatcher();

export default dispatcher;

 

Store.jsx에서는 dispatcher.register를 통해 디스패처로 전송되는 모든 페이로드와 함께 호출될 콜백을 등록합니다.

//Store.jsx
import { EventEmitter } from "events";
import dispatcher from "./Dispatcher";
import actionTypes from "./ActionTypes";

const CHANGE_EVENT = "change";
let _posts = [];

class PostStore extends EventEmitter {
    addChangeListener(callback) {
        this.on(CHANGE_EVENT, callback);
    }

    removeChangeListener(callback) {
        this.removeListener(CHANGE_EVENT, callback);
    }

    emitChange() {
        this.emit(CHANGE_EVENT);
    }

    getPosts() {
        return _posts;
    }
}

const store = new PostStore();

dispatcher.register((action) => {
    switch (action.actionTypes) {
        case actionTypes.GET_POSTS:
            _posts = action.posts;
            store.emitChange();
            break;
        default:
    }
});

export default store;

 

Post 페이지 및 PostLists

import React, { useState, useEffect } from "react";
import PostLists from "../components/PostLists";
import postStore from "../components/Store";
import { getPosts } from "../components/Actions";

function PostPage() {
    const [posts, setPosts] = useState(postStore.getPosts());

    useEffect(() => {
        postStore.addChangeListener(onChange);
        if (postStore.getPosts().length === 0) getPosts();
        return () => postStore.removeChangeListener(onChange);
    }, []);

    function onChange() {
        setPosts(postStore.getPosts());
    }

    return (
        <div>
            <PostLists posts={posts} />
        </div>
    );
}

export default PostPage;
//PostLists.jsx
import React from "react";

function PostLists(props) {
    return (
        <div style={{ margin: 20 }}>
            {props.posts.map((post) => (
                <div className="card mt-4" key={post.id}>
                    <h2 className="card-header">{post.title}</h2>

                    <div className="card-body">
                        <h6 className="card-subtitle mb-2 text-muted">
                            @{post.author}
                        </h6>
                        <blockquote className="blockquote mb-0">
                            <p>{post.body}</p>
                        </blockquote>
                    </div>
                </div>
            ))}
        </div>
    );
}

export default PostLists;

 

페이지를 구성할 코드들은 아래와 같습니다.

 

Home.jsx

//Home.jsx
import React from "react";

function Home() {
    return (
        <div className="jumbotron">
            <h1 class="display-4">블로그에 방문해주셔서 감사합니다</h1>
            <p class="lead">This is a Home page of BlogApp.</p>
        </div>
    );
}

export default Home;

 

NotFound.jsx

import React from "react";
import { Link } from "react-router-dom";

function NotFound() {
    return (
        <div>
            <h2>Page Not Found</h2>
            <Link exact to="/">
                Back to Home
            </Link>
        </div>
    );
}

export default NotFound;

 

Post.jsx

import React, { useState, useEffect } from "react";
import PostLists from "../components/PostLists";
import postStore from "../components/Store";
import { getPosts } from "../components/Actions";

function PostPage() {
    const [posts, setPosts] = useState(postStore.getPosts());

    useEffect(() => {
        postStore.addChangeListener(onChange);
        if (postStore.getPosts().length === 0) getPosts();
        return () => postStore.removeChangeListener(onChange);
    }, []);

    function onChange() {
        setPosts(postStore.getPosts());
    }

    return (
        <div>
            <PostLists posts={posts} />
        </div>
    );
}

export default PostPage;

 

Post 데이터를 갖고있을 db.json

{
    "actionTypes": "GET_POSTS",
    "posts": [
        {
            "id": 1,
            "title": "Hello World",
            "author": "Kyle",
            "body": "Example of blog application"
        },
        {
            "id": 2,
            "title": "Hello Again",
            "author": "John",
            "body": "Testing another component"
        }
    ]
}

 

NavBar.jsx

import React from "react";
import { NavLink } from "react-router-dom";

function NavBar() {
    return (
        <nav className="navbar navbar-light bg-light">
            <NavLink style={({ isActive }) =>
                isActive ? { color: "red" } : undefined
            } to="/">
                Home
            </NavLink>
            <NavLink style={({ isActive }) =>
                isActive ? { color: "red" } : undefined
            } to="/posts">
                Posts
            </NavLink>
        </nav>
    );
}

export default NavBar;

 

마지막으로 App.js 와 index.jsx 입니다.

// App.js
import React from "react";
import { Route, Routes, BrowserRouter } from "react-router-dom";

import Home from "./pages/Home";
import PostPage from "./pages/Post";
import NotFound from "./pages/NotFound";
import NavBar from "./components/common/NavBar";

function App() {
  return (
    <BrowserRouter>
      <NavBar />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/posts" element={<PostPage />} />
        <Route element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

 

//index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
// Bootstrap for styling
import "bootstrap/dist/css/bootstrap.min.css";

import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

 

코드를 모두 적용하게 되면 아래처럼 Home 을 확인할수있고, Posts를 눌렀을 경우 페이지 이동하여 포스트들이 보여지게 됩니다.

 

 

반응형