RESTful API를 활용한 Front-end 연결

JOY🌱·2023년 4월 30일
0

🌊 RESTful API

목록 보기
3/3
post-thumbnail

GitHub

📌 초기 셋팅

👉 .env

# REST API SERVER CONFIG
REACT_APP_RESTAPI_SERVER_IP=localhost
REACT_APP_RESTAPI_SERVER_PORT=8001

👉 src/modules/index.js

/* 여러 모듈을 combine 시키기 */
const rootReducer = combineReducers({
    productReducer, memberReducer, purchaseReducer, reviewReducer
});

export default rootReducer;

👉 src/Store.js

const store = createStore(
    rootReducer,
    composeWithDevTools(applyMiddleware(ReduxThunk, ReduxLogger))
);

export default store;

👉 src/index.js

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <Provider store={store}>
        <App />
    </Provider>
);

👀 화면 배치

👉 src/App.js

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={ <Layout/> }>
          <Route index element={ <Main/> }/>
          <Route path="products/categories/:categoryCode" element={ <Main/> }/>
          <Route path="search" element={ <Main/> }/>
          <Route path="products/:productCode" element={ <ProductDetail/> }/>
          <Route path="products/:productCode/purchase" element={<ProtectedRoute loginCheck={ true }><Purchase/></ProtectedRoute> }/>

          {/* Layout 내부에 MyPageLayout */}
          <Route path="mypage" element={ <MyPageLayout/> }>
            <Route index element={ <Navigate to="/mypage/profile" replace/> }/> {/* /mypage 경로로 진입했을 때 자동으로 /mypage/profile 페이지로 이동 */}
            <Route path="profile" element={ <ProtectedRoute loginCheck={ true }><Profile/></ProtectedRoute> }/>{/* (true일 때만 가능하도록) */} 
            <Route path="payment" element={ <ProtectedRoute loginCheck={ true }><Payment/></ProtectedRoute> }></Route>
          </Route>

          <Route path="products-management" element={ <ProtectedRoute authCheck={ true }><ProductManagement/></ProtectedRoute> }/>
          <Route path="products-registration" element={ <ProtectedRoute authCheck={ true }><ProductRegistration/></ProtectedRoute> }/>
          <Route path="products-update/:productCode" element={ <ProtectedRoute authCheck={ true }><ProductUpdate/></ProtectedRoute> }/>

          <Route path="review/:productCode" element={ <Review /> }/>
          <Route path="reviewDetail/:reviewCode" element={<ReviewDetail />}/>

        </Route>
  
        {/* 로그인 후, url로 접근하지 못 하도록 ProtectedRoute (false일 때만 가능하도록) */}
        <Route path="/login" element={ <ProtectedRoute loginCheck={ false }><Login/></ProtectedRoute> }/> 
        <Route path="/register" element={ <ProtectedRoute loginCheck={ false }><Register/></ProtectedRoute> }/>

        {/* 설정해둔 path가 아닐 경우 모두 Error 페이지로 이동 */}
        <Route path="*" element={ <Error/>}/>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

👉 src/layouts/Layout.js

function Layout () {

    const navigate = useNavigate();

    /* 로그아웃 버튼 클릭 시 로그아웃 */
    const onClickLogoutHandler = () => {
        window.localStorage.removeItem('accessToken'); // LocalStorage에서 accessToken을 삭제 시, 자동 로그아웃
        alert('로그아웃 후 메인으로 이동🥳');
        navigate("/", { replace : true });
    }

    return (
        <>
            <Header onClickLogoutHandler = { onClickLogoutHandler }/>
            <Navbar/>
            <main className={ LayoutCSS.main }>
                <Outlet/>
            </main>
            <Footer/>
        </>
    );
}

export default Layout;

👀 Pages

👉 src/pages/products/Main.js

function Main() {

    const dispatch = useDispatch();
    const products = useSelector(state => state.productReducer); // state를 useSelector로 꺼내옴
    console.log('products : ', products);
    const productList = products.data<;
    const pageInfo = products.pageInfo;

    const [currentPage, setCurrentPage] = useState(1);

    /* 카테고리별 요청 시 사용할 값 ----------------- */
    const params = useParams();
    const categoryCode = params.categoryCode;
    console.log("categoryCode : ", categoryCode);
    /* -------------------------------------------- */
    /* 검색어 요청 시 사용할 값 ---------------------*/
    const [searchParams] = useSearchParams();
    const search = searchParams.get('value'); // value라는 key값 이용
    console.log('search : ', search);
    /* -------------------------------------------- */

    useEffect(
        () => {
            if(categoryCode) {
                /* 카테고리별 음식에 대한 요청 */
                dispatch(callProductCategoriesListAPI({ categoryCode, currentPage }));
            } else if(search) {
                /* 검색어에 해당하는 음식에 대한 요청 */
                dispatch(callProductSearchListAPI({ search, currentPage }));
            } else {
                /* 모든 음식에 대한 요청 (CategoryCode가 따로 없고 undefined인 경우) */
                dispatch(callProductListAPI({ currentPage })); // API호출하는 function 호출
            }
        // [mount시점 뿐만 아니라 현재 페이지가 변경되어도 렌더링, categoryCode가 변경되어도 렌더링, search가 변경되어도 렌더링]
        },[currentPage, categoryCode, search] 
    );

    return (
        <>
            <div>
                { productList && <ProductList productList={ productList }/> }
            </div>
            <div>
                 {/* 현재 페이지를 조절하기 위한 setCurrentPage를 props로 보냄 */}
                { pageInfo && <PagingBar pageInfo={ pageInfo } setCurrentPage={ setCurrentPage }/> }
            </div>
        </>
    );
}
export default Main;

👀 Components

👉 src/components/common/Header.js

function Header({ onClickLogoutHandler }) {

    const navigate = useNavigate();
    const [search, setSearch] = useState('');

    /* 로고 클릭 시 메인 페이지로 이동 */
    const onClickLogoHandler = () => {
        navigate("/");
    }

    /* 검색어 입력 값 상태 저장 */
    const onSearchChangeHandler = (e) => {
        setSearch(e.target.value);
    }

    /* Enter키 입력 시 검색 화면으로 넘어가는 이벤트 */
    const onEnterKeyHandler = (e) => {
        if(e.key = 'Enter'){
            console.log('Enter key : ', search);
            navigate(`/search?value=${search}`);
        }
    }

    function BeforeLogin() {
        return (
            <div>
                <NavLink to="/login" className={ HeaderCSS.font }>로그인</NavLink> | <NavLink to="/register" className={ HeaderCSS.font }>회원가입</NavLink>
            </div>
        );
    }

    function AfterLogin() {

        const onClickMypageHandler = () => {
            navigate("/mypage");
        }

        return (
            <div >
                <button
                    className={ HeaderCSS.HeaderBtn }
                    onClick={ onClickMypageHandler }
                >
                    마이페이지
                </button> |&nbsp;
                <button
                    className={ HeaderCSS.HeaderBtn }
                    onClick={ onClickLogoutHandler }
                >
                    로그아웃
                </button>
            </div>
        );
    }

    return (
        <>
            <div className={ HeaderCSS.HeaderDiv }>
                <button 
                    className={ HeaderCSS.LogoBtn }
                    onClick={ onClickLogoHandler }
                >
                    GREEDY
                </button>
                <input 
                    className={ HeaderCSS.InputStyle } 
                    type="text" placeholder="검색"
                    onChange={ onSearchChangeHandler }
                    onKeyUp={ onEnterKeyHandler }
                />
                { isLogin() ? <AfterLogin/> : <BeforeLogin/> } {/* TokenUtils의 isLogin함수를 불러와 로그인 여부 체크 */}
            </div>
        </>
    );
}

export default Header;

👉 src/components/common/Navbar.js

function Navbar() {

    const style = { textDecoration : 'none', color : 'salmon', fontWeight : 'bold' };
    const activeStyle = ({ isActive }) => isActive ? style : undefined;

    return (
        <div className={ NavbarCSS.NavbarDiv }>
            <div className={ NavbarCSS.NavlistUl }>
                <li><NavLink to="/" style={ activeStyle }>모든 음식</NavLink></li>
                <li><NavLink to="/products/categories/1" style={ activeStyle }>식사</NavLink></li>
                <li><NavLink to="/products/categories/2" style={ activeStyle }>디저트</NavLink></li>
                <li><NavLink to="/products/categories/3" style={ activeStyle }>음료</NavLink></li>
                { isAdmin() && <li><NavLink to="/products-management" style={ activeStyle }>상품등록</NavLink></li> }
            </div>
        </div>
    );
}

export default Navbar;

👉 src/components/common/Footer.js

function Footer() {

    return (
        <div className={ FooterCSS.footerDiv }>
            <h3 style={ { width: '100%', textAlign: 'center'} }>
                Copyright 2023. Greedy All rights reserved.
            </h3>
        </div>
    );
}

export default Footer;

👉 src/components/common/PagingBar.js

function PagingBar({ pageInfo, setCurrentPage }) {

    /* 내가 가진 startPage부터 endPage까지의 페이징바의 숫자를 반복문으로 출력 */
    const pageNumber = [];
    if(pageInfo) {
        for(let i = pageInfo.startPage; i <= pageInfo.endPage; i++) {
            pageNumber.push(i);
        }
    }

    /* 각 버튼을 누를 때 마다 setCurrentPage로 state를 변경하여 페이지 마다의 다른 메뉴들 노출 */
    return (
        <div style={{ listStyleType: 'none', display: 'flex', justifyContent: 'center' }}>
            <button 
                className={ PagingBarCSS.pagingBtn }
                onClick={ () => setCurrentPage(pageInfo.currentPage - 1) }
                disabled={ pageInfo.currentPage <= 1 } /* 1보다 작거나 같지 않을 때에만 실행하도록 설정 */
            ></button>
            { pageNumber.map(num => (
                <li key={num} onClick={ () => setCurrentPage(num) }>
                    <button 
                        className={ PagingBarCSS.pagingBtn }
                        /* 현재 페이지가 어디인지 알아와서 따로 CSS 속성 추가 */
                        style={ pageInfo.currentPage === num ? { backgroundColor : 'salmon'} : null }
                    >
                        {num}
                    </button>
                </li>
            ))
            }
            <button 
                className={ PagingBarCSS.pagingBtn }
                onClick={ () => setCurrentPage(pageInfo.currentPage + 1) }
                disabled={ pageInfo.currentPage >= pageInfo.maxPage } /* 맨 끝 페이지보다 크거나 같지 않을 때에만 실행하도록 설정 */
            ></button>
        </div>
    );
}

export default PagingBar;

👉 src/components/items/ProductItem.js

function ProductItem ({ product : { productCode, productImageUrl, productName, productPrice }}) {

    const navigate = useNavigate();

    const onClickProductHandler = (productCode) => {
        navigate(`/products/${productCode}`);
    }

    return (
        <div 
            className={ ProductCSS.productDiv }
            onClick={ () => onClickProductHandler(productCode) }
        >
            <img src={ productImageUrl } alt={ productName }/>
            <h5>{ productName }</h5>
            <h5>{ productPrice }</h5>
        </div>
    );
}

export default ProductItem;

👉 src/components/lists/ProductList.js

function ProductList ({ productList }) {

    return (
        <div className={ ProductListCSS.productDiv }>
            {
                Array.isArray(productList)
                && productList.map(product => <ProductItem key={ product.productCode } product={ product }/>)
            }
        </div>
    );
}

export default ProductList;

👀 APIs

👉 src/apis/ProductAPICalls.js

const SERVER_IP = `${ process.env.REACT_APP_RESTAPI_SERVER_IP }`;
const SERVER_PORT = `${ process.env.REACT_APP_RESTAPI_SERVER_PORT }`;
const PRE_URL = `http://${SERVER_IP}:${SERVER_PORT}/api/v1`;

/* 모든 음식에 대한 요청 (CategoryCode가 따로 없고 undefined인 경우) */
export const callProductListAPI = ({ currentPage = 1 }) => {

    const requestURL = `${PRE_URL}/products?page=${currentPage}`;

    return async (dispatch, getState) => {

        const result = await fetch(requestURL).then(response => response.json());

        if(result.status == 200) {
            console.log('[ProductAPICalls] : callProductListAPI result', result);
            dispatch(getProducts(result)); // 액션 함수 호출
        } 

    }

}

/* 카테고리별 음식에 대한 요청 */
export const callProductCategoriesListAPI = ({ categoryCode, currentPage = 1 }) => {

    const requestURL = `${PRE_URL}/products/categories/${categoryCode}?page=${currentPage}`;

    /* redux thunk 안에서 호출될 함수들 아래에 정의 */
    return async (dispatch, getState) => { 

        const result = await fetch(requestURL).then(response => response.json());

        if(result.status == 200) {
            console.log('[ProductAPICalls] : callProductCategoriesListAPI result', result);
            dispatch(getProducts(result));
        }

    }
}

/* 검색어에 해당하는 음식에 대한 요청 */
export const callProductSearchListAPI = ({ search, currentPage = 1 }) => {

    const requestURL = `${PRE_URL}/products/search?productName=${search}&page=${currentPage}`;

    /* redux thunk 안에서 호출될 함수들 아래에 정의 */
    return async (dispatch, getState) => { 

        const result = await fetch(requestURL).then(response => response.json());

        if(result.status == 200) {
            console.log('[ProductAPICalls] : callProductSearchListAPI result', result);
            dispatch(getProducts(result));
        }
    }
}

/* 선택한 음식의 상세 정보에 대한 요청 */
export const callProductDetailAPI = ({ productCode }) => {

    const requestURL = `${PRE_URL}/products/${productCode}`;

    /* redux thunk 안에서 호출될 함수들 아래에 정의 */
    return async (dispatch, getState) => { 

        const result = await fetch(requestURL).then(response => response.json());

        if(result.status == 200) {
            console.log('[ProductAPICalls] : callProductDetailAPI result', result);
            dispatch(getProductDetail(result));
        }
    }
}

/* 모든 음식에 대한 요청 (관리자) */
export const callProductListForAdminAPI = ({ currentPage = 1 }) => {

    const requestURL = `${PRE_URL}/products-management?page=${currentPage}`;

    return async (dispatch, getState) => {

        const result = await fetch(requestURL, {
            method : 'GET',
            headers : {
                "Content-Type" : "application/json",
                "Authorization" : "Bearer " + window.localStorage.getItem('accessToken')
            }
        })
        .then(response => response.json());

        if(result.status == 200) {
            console.log('[ProductAPICalls] : callProductListForAdminAPI result', result);
            dispatch(getProducts(result)); // 이전의 ProductsList를 받아오는 액션 함수를 그대로 사용
        }

    }

}

/* 상품 등록 (관리자) */
export const callProductRegistAPI = ( formData ) => {

    const requestURL = `${PRE_URL}/products`;

    return async (dispatch, getState) => {

        const result = await fetch(requestURL, {
            method : 'POST',
            headers : {
                "Authorization" : "Bearer " + window.localStorage.getItem('accessToken') 
            },
            body : formData
        })
        .then(response => response.json());

        if(result.status == 200) {
            console.log('[ProductAPICalls] : callProductRegistAPI result', result);
            dispatch(postProduct(result));
        }

    }

}

/* 상품 상세 페이지 및 수정 (관리자) */
export const callProductDetailForAdminAPI = ({ productCode }) => {

    const requestURL = `${PRE_URL}/products-management/${productCode}`;

    /* redux thunk 안에서 호출될 함수들 아래에 정의 */
    return async (dispatch, getState) => { 

        const result = await fetch(requestURL, {
            method : 'GET',
            headers : {
                "Authorization" : "Bearer " + window.localStorage.getItem('accessToken')
            } 
        }).
        then(response => response.json());

        if(result.status == 200) {
            console.log('[ProductAPICalls] : callProductDetailForAdminAPI result', result);
            dispatch(getProductDetail(result)); // 이전의 ProductDetail을 받아오는 액션 함수를 그대로 사용
        }
    }
}

export const callProductUpdateAPI = (formData) => {

    const requestURL = `${PRE_URL}/products`;

    return async (dispatch, getState) => {

        const result = await fetch(requestURL, {
            method : 'PUT',
            headers : {
                "Authorization" : "Bearer " + window.localStorage.getItem('accessToken') 
            },
            body : formData // 파일을 포함했기 때문에 JSON 형태가 아닌 formData 형식
        }).then(response => response.json())

        if(result.status === 200) {
            console.log('[ProductAPICalls] callProductUpdateAPI result :', result);
            dispatch(putProduct(result));
        }
    }
}

👀 Modules

👉 src/modules/ProductModule.js

/* 초기값 */
const initalState = [];

/* 액션 : 액션 생성 함수를 정의하는 코드 */
const GET_PRODUCTS = "product/GET_PRODUCTS";
const GET_PRODUCT_DETAIL = "product/GET_PRODUCT_DETAIL";
const POST_PRODUCT = 'product/POST_PRODUCT';
const PUT_PRODUCT = 'product/PUT_PRODUCT'

export const { product : { getProducts, getProductDetail, postProduct, putProduct } } = createActions({
    // (res) => res.data : getProducts 함수를 호출하고 전달 받게 되는 payload 값
    // { type: 'product/GET_PRODUCTS', payload: res.data }와 같은 형태의 액션 객체를 반환
    [GET_PRODUCTS] : (res) => res.data,
    [GET_PRODUCT_DETAIL] : (res) => res.data,
    [POST_PRODUCT] : (res) => res,
    [PUT_PRODUCT] : (res) => res
});

/* 리듀서 */
const productReducer = handleActions( // 데이터 값이 state로 저장이 됨
    {
        // { [액션타입] : (이전상태, 액션) => 새로운상태 }
        // payload 값을 새로운 상태로 반환하여 state로 저장
        [GET_PRODUCTS] : (state, { payload }) => payload, // { payload } : res.data를 의미
        [GET_PRODUCT_DETAIL] : (state, { payload }) => payload,
        [POST_PRODUCT] : (state, { payload }) => ({ regist : payload }), // 구분하기 위해 regist라는 key값 설정
        [PUT_PRODUCT] : (state, { payload }) => ({ modify : payload })
    }
, initalState);

export default productReducer; 
profile
Tiny little habits make me

0개의 댓글