반응형

이번에 리액트로 쇼핑몰 프로젝트를 연습삼아 만들어보게되면서 닷홈의 무료 호스팅을 신청해서 

그 서버에 빌드한 파일을 올리는 방법으로 호스팅해주었다. 과정은 간단한데 로컬에서 파일을 빌드하고 닷홈서버의 html폴더에 빌드한 파일을 넣어주기만 하면되는거라서 간단하게 자동화가 가능할것같았다. 그래서 github action을 이용해서 업데이트 자동화를 해보기로 했다. 

 

우선 깃허브에 코드를 올려주고 해당 리포지토리로 이동해준다.

리포지토리에서 Actions로가서 simpleworkflow를 검색한다음 configure를 클릭한다.

그러면  생성한 리포지토리의 .github/workflows 경로에 blank.yml 파일을 생성해주게 된다.

이제 이파일에 필요한 동작들을 작성해주면 코드가 푸쉬됐을때 깃허브에서 띄워주는 가상의 컴퓨터에서 입력한 동작들을 수행해준다 

 

내가작성한 코드를 올려줄테니 참고해서 닷홈에 자동화로 올리는 걸 많이 시도해봤으면 좋겠다.

name: CI/CD Pipeline

on:
  push:
    branches:
      - main # 메인 브랜치가 푸쉬될때 동작

jobs:
  build:
    runs-on: ubuntu-latest # 사용할 os 버전 

    steps:
    - name: Check out code
      uses: actions/checkout@v2

    - name: Set up Node.js
      uses: actions/setup-node@v2
      with:
        node-version: '18' # 사용할 Node.js 버전을 설정합니다.

    - name: Install dependencies
      run: npm install # 프로젝트의 의존성 설치 

    - name: Build project
      run: npx vite build # 프로젝트 빌드 명령어를 실행합니다.

    - name: List dist directory contents
      run: ls -al dist # 빌드된 파일의 폴더로 이동 
      
    - name: Install LFTP
      run: |
        sudo apt-get update
        sudo apt-get install -y lftp # ftp 프로그램 설치 

    - name: Transfer files via SCP
      env:
        SERVER_HOST: ${{ secrets.SERVER_HOST }}
        SERVER_USERNAME: ${{ secrets.SERVER_USERNAME }}
        SERVER_PASSWORD: ${{ secrets.SERVER_PASSWORD }}
      run: 
        lftp -u  ${{ secrets.SERVER_USERNAME }},${{ secrets.SERVER_PASSWORD }} ftp://${{ secrets.SERVER_HOST }}:21 -e "
        mirror -R dist /html;
        bye
        "

 

이렇게 설정하면 원격컴퓨터에서 노드를 설치하고 파일의 의존성을 설치한다음 빌드 하고 해당 파일을 원격지의 닷홈서버로 ftp프로토콜로 전송하게되는 코드이다. 

vite를 사용해서 dist 폴더에 저장되는것같은데 build명령어나 의존성을 설치하는명령어는 사용하는 패키지 매니저나 명령어에 따라서 다르기때문에 자신의 환경에 맞춰서 써주면 될것같다. yarn을 이용하는사람은 yarn 명령어를 사용해야할테고.. 

 

닷홈은 ssh 를 열어놓지않기때문에 수작업으로 할때는 빌드한 파일을 파일질라로 닷홈서버에 옮겼는데 이렇게하면 메인 브랜치에 코드가 푸쉬될때 자동으로 빌드한파일을 닷홈의 /html폴더로 이동시켜서 코드만올려도 자동으로 업데이트 되는 효과를 볼수있다. 프로젝트가 복잡해지면 다른 추가적인 과정도 필요하게 될수도있지만 현재로서는 잘 동작하는듯하다.

반응형
반응형

프로젝트를 만들려고 다시 옛날에 만들었던 코드를 보니 디자인이나 구성요소를 가져가기에는 
맘에 들지않는 부분이많아서 디자인이나 기능 부분도 새로 만들어보기로 했다. 

 

참고로 나는 디자인에 소질이 정말 없다고 자부하는데 고르는 색깔마다 별로 좋은 반응을 받은적이없어서 AI툴을 이용해보기로 했다 

 

https://v0.dev/

 

v0 by Vercel

Chat with v0. Generate UI with simple text prompts. Copy, paste, ship.

v0.dev

next.js 를 만들고 vercel 을 만드는 곳에서 개발한 AI툴인데 프롬프트에 필요한 디자인을 입력하면 
해당 컴포넌트와 필요한부분들이 모두 코딩되어서 코드수준으로 만들어준다. 

우선 내가 필요한 페이지를 정리해 보았다. 

가장 먼저 보여지는 index 페이지는 베스트 셀러나 추천하는 상품과 배너가들어갈수있는 페이지로 구성하고 

각각 페이지들은 상품의 카테고리로 들어갈수있는 전형적인 쇼핑몰 형태의 페이지로 구성한다

추가적으로 관리자 페이지가 필요한데 관리자의 index페이지는 세일즈코스트나 잘 팔리는 상품들을 그래프로 조회할수있는 패널형태의 페이지로 구성하고 각각 페이지에 관리자에게 필요한 기능을 넣은 페이지로구현하려고 한다.

 

관리자에게 필요한기능은 구현하면서 계속 추가될것같지만 우선적으로 필요한기능은 

1. 매출액 조회 (월단위 , 일단위 , 지정한 시작 종료일 기준)

2. 상품등록 , 삭제 , 수정 

3. 회원관리

4. 재고관리 

5. 할인또는 프로모션의 관리 

6. 공지 기능 

벌써 꽤 머리가 아플것같은 기능들이있는데 아마 진행하면서 더 늘어날수도있을것같다. 한 번에 다 구현은 할수없으니 하나씩 천천히 구현 해봐야할것같다. 현재로써는 상품에 댓글 기능은 포함하지않는 기획이라 댓글이나 답글은 없어도될듯하다. 사실 최초 기획단계에서 필요한 요소를 모두 정의하고 들어가면 좋겠지만 코딩을 공부하는 입장에서 그렇게까지하면 코드를 공부하는 시간보다 기획에 시간을 너무 쏟게 될것같아서 기능이 추가될수있다는 점만 염두에두고 확장성을 고려하여 코드작업을 해보도록 하겠다. 그쪽이 더 공부가 될것같고... 이전 기획과 새로운 기획에서 충돌이 났을떄의 해결도 좋은 경험이 될것같다.

 

나는 꼭 vite + typesciript + react  연습을하기 위해서 v0.dev로 만들고 react 만 사용하는 환경으로 옮기느라 고생을좀했지만 next.js프레임워크를 사용한다면 v0.dev가 만든 코드를 그대로 프로젝트로 생성해서 사용할수있어서 더 편하게 이용할수있다 .

 

해당 페이지는 빌드해서 호스팅사이트에 올려놓았다. 무료 호스팅이라 비용없이 간단하게 이용할수있어서 좋은것같다

corona456.dothome.co.kr

 

Vite + React + TS

 

corona456.dothome.co.kr

 

v0.dev 로만든 소스코드도 깃허브에 올려놓았으니 설정에 어려움이있다면 참고하자 

https://github.com/cokeholic-kim/Univershop

 

GitHub - cokeholic-kim/Univershop

Contribute to cokeholic-kim/Univershop development by creating an account on GitHub.

github.com

 

반응형
반응형

작년 초에 학원에서 리액트로 쇼핑몰을 만드는 수업을 받았었다.  

다시 리액트를 사용하려고보니 빌드툴도 vite라는 것을 사용하고 상태관리쪽도 새로운 라이브러리가 떠오르고있는듯해서 

다시 연습하는겸 쇼핑몰 프로젝트를 다시만들어보려고한다. 

지금 다니는 회사에서도 리액트 , 타입스크립트 , 넥스트 같은 기술들을 도입하려고 준비하다보니 따라가는 입장에서 연습을 해보고 사용할수있는 api도 회사에서 사용하는 기술인 fastAPI 프레임워크를 통해서 만들어보겠다. 

 

1. 프로젝트 세팅

npm create vite@latest 프로젝트이름

프레임워크와 옵션을 선택할수있게 나올텐데 방향키로 움직여서 원하는 옵션에서 엔터를 누르면 해당 옵션과 프레임워크가 설치된다.

 

나는 리액트 프레임워크에 타입스크립트와 swc를 사용하게 설정해주었다. 

 

이제 vscode로 프로젝트를 열어주고 프로젝트를 실행하기위해 라이브러리를 설치해준다.

npm i

 

라이브러리들이 다 설치되면 잘 실행되는지 확인해본다.

npm run dev

이렇게 running 되었다고 뜰텐데 저 localhost 주소로 들어가보면 페이지가 정상적으로 동작하는지 확인하면 된다.

이렇게하면 리액트 프로젝트를 시작할준비가 끝이났다. 

반응형
반응형

Redux-middleware

리덕스를 사용하는 이유 중에 하나는 미들웨어이다.

리덕스 미들웨어를 사용하면 액션을 디스패치하고 그 액션이 리듀서에 가기 전에 추가적인 작업을 더 해줄 수 있다. 

middleware의 실행위치

그래서 보통 비동기 방식으로 사용할 때 미들웨어를 많이 사용한다고 한다.

(서버에서 액션을 받아오면 리듀서에 가기 전에 비동기로 데이터를 받아서 보내줘야 하기 때문). 

 

리덕스 미들웨어는 개인이 필요한 용도로 만들어서 쓸 수도 있지만

보통은 만들어진 미들웨어를 받아와서 사용한다.

redux-thunk

redux-saga

redux-observable

redux-promise-middleware 등이 있다.

 

이번 포스팅에서는 리덕스 미들웨어가 어떤 건지 파악만 하기 때문에

직접 미들웨어를 만들어보고 대강의 구조를 파악해 보겠다.

 

1. 리듀서에 가기 전 추가적인작업

dispatch({ type:"add_todo" })


리덕스 미들웨어를 사용하면 액션이 디스패치된 다음,
리듀서에서 해당 액션을 받아서 업데이트하기 전에 
추가적인 작업을 할 수 있음. 

서버에 있는 값을 받아서 업데이트해 줄 때 주로 사용.(비동기)

 

간단한 리덕스 모듈을 만들고 미들웨어를 만들어서 적용해 보겠다.

 

1. 리덕스 모듈 만들기

액션타입, 액션생성함수 리듀서

modules/counter.js

//액션타입
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

//액션생성함수 --> 액션을 리턴
export const increase = ()=>({type:INCREASE})
export const decrease = ()=>({type:DECREASE}) 


//초기값
const initialState = 0

//리듀서
export default function counter(state=initialState,action){
    switch(action.type){
        case INCREASE:
            return state+1;
        case DECREASE:
            return state-1;
        default:
            return state;
    }
}

 

2. 루트 리듀서 만들기

모듈이 여러 개일 때는 리듀서가 여러 개이기 때문에 리듀서들을 하나로 합쳐서 store를 만들 때 사용한다.

modules/index.js

import { combineReducers } from "redux";
import counter from "./counter";

const rootReducer = combineReducers({ counter })

export default rootReducer

 

3. 스토어 만들기 , 전역에 스토어 사용하기

만들어진 rootreducer를 이용해서 스토어를 만들고 스토어를 사용할 수 있게 하위컴포넌트들을 묶어준다.

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';

//스토어 만들기.
const store = createStore(rootReducer)
console.log(store.getState()) //상태값 조회
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>
);

// 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();

스토어를 만들었으니 상태를 관리할 준비는 다 끝났고 이제

counter의 상태를 이용해서 간단한 화면을 구현해 보자

 

화면적으로 보이는 프레젠테이셔널컴포넌트를 먼저 만들고

값을 전달해 줄 컨테이너컴포넌트를 만들어주겠다.

 

component/Counter.js

import React from 'react';

const Counter = ({number,onIncrease,onDecrease}) => {
    return (
        <div>
            <h2>{number}</h2>
            <button onClick={onIncrease}>+</button>
            <button onClick={onDecrease}>-</button>
        </div>
    );
};

export default Counter;

화면에 보여줄 현재 숫자를 number로 받아오고 버튼을 클릭했을 때

상태값을 + , - 해줄 함수로 onIncreaseonDecrease를 받아왔다.

 

이제 이 값들을 보내줄 컨테이너 컴포넌트를 보자.

containers/CounterContainer.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Counter from '../components/Counter';
import { decrease, increase } from '../modules/counter';


const CounterContainer = () => {
    const number = useSelector(state=>state.counter)
    const dispatch = useDispatch()
    const onIncrease=()=>{
        dispatch(increase()) //export const increase = ()=>({type:INCREASE})
    }
    const onDecrease=()=>{
        dispatch(decrease()) //export const decrease = ()=>({type:DECREASE}) 
    }
    return (
        <div>
            <Counter number={number} onIncrease={onIncrease} onDecrease={onDecrease}/>
        </div>
    );
};

export default CounterContainer;

useSelector counter의 상태값을 number로 받아서 Counter컴포넌트props로 보내준다.

useDispatchdispatch함수를 받아와서 디스패치를 실행하는 함수를 만들었다 onIncrease ,onDecrease 

dispatch에 액션객체로는 액션생성 함수를 넣어줬다.

이렇게 만들어준 함수를 Counter컴포넌트props로 넘겨준다.

 

이제 이 컨테이너 컴포넌트를 app.js에 넣어서 렌더링만 해주면 된다.

 

 

이제 여기에 미들웨어를 하나 만들어서 값이 리듀서로 가기 전에 작업을 할 수 있게 해 주겠다.

 

 

*리덕스 미들웨어 
미들웨어는 함수를 연달아서 두 번 리턴하는 함수.

const middleware = store => next => action =>{
	//하고싶은작업..
}

function middleware(store){
	return function(next){
		return function(action){
			//하고싶은작업..
		}
	}
}

위와 같은 구조가 미들웨어의 구조이다 store는 파라미터로 들어가고 함수를 두번 리턴해준다.

 

각 함수에서 받아오는 파라미터
-store 리덕스 스토어 store dispatch( ) / getState( ) / subscribe( ) 내장함수 들어있음
-next 액션을 다음 미들웨어에 전달하는 함수
ex) next(action)
-action 은 현재 처리하고 있는 액션객체

-next(action)을 해주지 않으면 action이 reducer나 다음 미들웨어로 넘어가지 않아서 에러가 생긴다.

 

middlewares/myLogger.js

const myLogger = store => next =>action=>{
    //액션 출력하기
    console.log(action)
    //next는 다음미들웨어 또는 리듀서 에게 액션을 전달.
    const result = next(action); //다음 미들웨어가 없어서 리듀서가호출되고 상태값이 변함
    // 업데이트 이후의 상태를 출력
    console.log(store.getState());
    //여기에서 반환하는 result값은 dispatch(action)의 결과물.
    return result
}

export default myLogger;

지금 보내주는 액션을 콘솔에 출력해 주고 action을 다음미들웨어로 넘겨준다.

미들웨어로 action이 넘어간 다음 바뀐 상태값을 출력해 준다.

 

이제 만들어진 이 미들웨어를 사용해 보자

store를 만들 때 미들웨어를 같이 넣어주면 된다.

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { applyMiddleware , legacy_createStore as createStore } from "redux";
import { Provider } from "react-redux";
import App from "./App";
import rootReducer from "./modules";
import myLogger from './middlewares/myLogger';


const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
const store = createStore(rootReducer,applyMiddleware(myLogger));
console.log(store.getState());
root.render(
  <StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </StrictMode>
);

applyMiddlewareimport 해서 createstore안에 넣어주고 사용할 미들웨어를 파라미터로 넣어준다.

 

 

콘솔을 확인해 보면 어떤 액션이 들어가고 상태값이 어떻게 변하는지 미들웨어에서 출력을 해준다

이렇게 리듀서로 들어가는 자료들을 중간에서 받아서 출력해 줄 수도 있고 변경해서 다시 보내줄 수도 있는 게 미들웨어의 역할이다.

 

 

만들어진 미들웨어 써보기

redux-logger

npm install --save react-redux

redux-logger는 만들어져 있는 미들웨어이며 이전상태값과 액션 다음상태값을 출력해준다.

 

원래 저기에 이전상태값이랑 action객체 다음 상태값이 다 떠야되는데 왜 안뜨는건지... 

logger콘솔화면

원래는 이렇게 상태값과 액션객체가 뜬다.

 

 

피드백

사실 미들웨어는 저번주 금요일에 배웠던 개념인데 이해가 너무안되서 정리를 좀더 해보고 오늘 학원에서도 다시해보고 thunk를 배우면서 어느정도 개념에 대해서 받아들여진것같다. 역시 코딩은 안되면 일단 반복하면 되는구나.. 

다음 포스팅은 thunk에 대해서 할것같은데 이것도 아직 제대로 알아먹은건아니라서 두루뭉술하게 될지도. 일단 인강이나 이런거 찾아보고 내 뇌속의 개념을 조금더 보충해서 알아보기쉬운 포스팅을 쓸려고 하고있다. 그냥 코드만 따라하는건 아무나 하는거고 코드를 보고 이해할수있는게 중요한것같다. 이제 포트폴리오 준비도 해야되고 정신도 없지만 . 배우는 개념들은 모두 놓치지않고 이해하고싶다.

반응형
반응형

여기까지가 어제 구현 한 부분이다

modules에서 redux모듈counter.jstodos.js를 만들고 index.js에서 combinedReducers로 합쳐서 rootReducer를 만들어 준 후에 rootReducer를 이용해서 store를 만들어주고

만들어진 store를 컨테이너컴포넌트인 CounterContainers.js 에서 접근해서 상태값과 디스패치를 받아서

프레젠테이션컴포넌트인 Counter.js에게 props로 필요한 값을 전달해서

상태값dispatch함수를 화면구현에 사용했다.

 

오늘은 여기에 저번에 Redux모듈로 만들어놓고 사용하지 않은 todos를 이용하는 todolist를 만들어주겠다

 

화면을 구성하는 요소인 프레젠테이션 컴포넌트부터 만들었다.

 

components / Todos.js

import React, { useState } from 'react';
import Todolist from './Todolist';

const Todos = ({onCreate,onToggle,todos}) => {
    const [text,setText] = useState("") //input에 들어갈 상태값.
    const onChange = (e) =>setText(e.target.value) //input태그에 값이 입력되면 상태값이변함
    const onClick = () =>{
        onCreate(text);
        /*
        (text) =>({
        	type:ADD_TODO,
        	todo:{id:nextId,text: text,done:false}
    	}) 
        */
        setText(''); // 등록한후에는 input창을 초기화하기위해서 상태값 비워줌
    }
    return (
        <div>
            <div>
                <input value={text} onChange={onChange}></input>
                <button onClick={onClick}>등록</button> 
            </div>
                <Todolist onToggle={onToggle} todos={todos}/>
        </div>
    );
};

export default Todos;

인풋과 등록창을 만들어주고 등록되었을때 화면에 뜨는 부분은 Todolist컴포넌트로 따로 만들었다.

input 태그에도 상태값을 하나 추가로 주기위해 따로 useState를 이용해서 상태값을 만들어줬다.

 

이제 할일을 추가했을 때 띄워줄 Todolist컴포넌트를 만들어준다.

 

components / Todolist.js

import React from 'react';

const Todolist = ({todos,onToggle}) => {
    return (
        <div>
            <ul>
                {todos.map(todo => <li key={todo.id} 
                onClick={()=>onToggle(todo.id)} 
                style={{textDecoration: todo.done ? 'line-through':'none'}}>
                    {todo.text}
                </li>)}
            </ul>
        </div>
    );
};

export default Todolist;

Todos컴포넌트에서 onClick해주면 상태값의 todos에 저장된다.

case ADD_TODO:
   return[
      ...state, //원래있던값들을 저장하고
      action.todo // todo:{id:nextId,text: text , done:false} 새로운 todo추가 
    ]

todosTodolist.js에 넘겨주고 배열메서드인 map으로 <li></li> 안에 값을 넣어서 만들어준다. 

li를 클릭하면 isDone이 토글되고

isDone이 false일 때는 유지하고 true가 되면 밑줄을 그어주도록 스타일도 적용해 준다.

style={{textDecoration: todo.done ? 'line-through':'none'}}

 

isDone을 토글 시켜줄 onToggle TodosContainer에서 만들어서 전달된다.

onToggle아이디를 전달받아서

case TOGGLE_TODO:
     return state.map(todo => todo.id === action.id ? {...todo, done:!todo.done} : todo )
default:

todos를 다시 맵으로 돌려서 해당하는 id와 같은 아이디가 있으면 todo.done을 토글 시켜준다.

 

 

 

어제 했던 redux를 이어서 한 건데 코드를 한번 더 읽어보고 다시 써보니까 이제 동작원리가 조금은 이해가 되는 것 같다.

여기저기서 불러쓰는 게 많아서 복잡해보였는데 불러서 쓰는 요소들을 밑에 주석으로 써가면서 보니까 읽기에도 편하고 뭐하는 함수였는지 바로바로 알수있었다. 주석은 좀 더 간결하게 쓰는게 좋을 것 같지만 일단은 내가 이해를 해야 하니 길어도 어쩔 수 없다..ㅋㅋㅋ

반응형
반응형

오늘은 react에서 상태를 관리해 주는 기술react-redux를 배웠다.

 

Redux

 

Redux 시작하기 | Redux

소개 > 시작하기: Redux를 배우고 사용하기 위한 자료

ko.redux.js.org

Redux(리덕스)는 JavaScript(자바스트립트) 상태관리 라이브러리이다.

(react에서뿐만 아니라 바닐라 자바스크립트에서도 사용가능)

 

redux의 규칙 3가지

1. 하나의 애플리케이션 안에는 하나의 스토어가 있다.

2. 상태는 읽기 전용이다.

3. 리듀서는 순수한 함수여야 한다.

 

redux사용방법

//스토어 생성하기
const sotre = Redux.createStore(리듀서함수)
redux작동 그래프

액션 객체를 dispatch로 보내주면 dispatch는 스토어에 있는 reducer함수를 불러서

함수에 액션객체를 매개변수로 새로운 상태값을 반환해 준다.

 

1. 액션객채(Action)

상태를 업데이트할 때 액션을 전달해 준다.

액션은 객체로 표현되며 type속성을 가지고 있다.

{
	type:"INCREMENT"
}

2. 액션 생성함수(Action creator)

액션객체를  퉤! 하고 뱉어주는 함수

function increment(add){
	return{
		type:"INCREMENT",
		add: add
	}
}

useReducer에서도 액션을 사용했지만 액션을 함수로 만들어서 사용한 적은 없었는데.

이렇게 사용하면 동일한 액션객체를 전달해줄 수 있고 매개변수를 받아서 액션객체에 추가해 줄 수도 있다.

 

3. 리듀서(reducer)

상태를 업데이트시켜주는 함수이다.

리듀서는 두 가지 파라미터를 받아온다.( 상태값, 액션객체 )

function reducer(state,action){
	return newState;
}

리듀서에서는 새로운 상태값을 리턴해준다.

 

4. 스토어(store)

리덕스에서는 한  애플리케이션당 하나의 스토어를 만든다.

스토어 안에는 현재의 상태와 리듀서 내장함수들이 들어가 있다.

dispatch(), subscribe(), getState()

 

5. 구독(subscribe)

스토어의 내장함수 중 하나이다. 함수 형태의 값을 파라미터로 받아오며 subscribe함수에

특정 함수를 전달하면 액션이 디스패치 될 때마다 전달해 준 함수가 호출됨.

//자바스크립트에서는 상태가 변해도 리렌더링 되지 않기 때문에 상태값이 변할 때 함수를 다시 호출해서 바뀐 값을 다시 넣어서 실행해줘야 한다.

 

 

여기서 redux를 가져올 주소를 복사해서 redux를 사용할 파일에 연결해 준다.

<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.min.js"></script>

이제 redux의 기능들을 해당 애플리케이션 파일에서 사용할 수 있다.

redux의 필요성을 알아보기 위해 같은 기능을

redux를 사용하지 않고 구현한 코드와 사용해서 구현한 코드 두 개를 쓰겠다.

 

withoutredux.html (redux 없이)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    
    <style>
        .component{
            border: 5px solid #000;
            padding:10px;
        }
    </style>
</head>
<body>
    <div id="red"></div>
    <div id="blue"></div>
    <script>
        function red(){
            document.querySelector("#red").innerHTML = `
            <div class="component">
                <h1>red</h1>
                <button onclick="
                document.querySelector('#red').style.backgroundColor='red'
                document.querySelector('#blue').style.backgroundColor='red' ">버튼</button>    
            </div>
            `
        }    
        function blue(){
            document.querySelector("#blue").innerHTML = `
            <div class="component">
                <h1>blue</h1>
                <button onclick="
                document.querySelector('#blue').style.backgroundColor='blue'
                document.querySelector('#red').style.backgroundColor='blue'">버튼</button>    
            </div>
            `
        }  

        red();
        blue();  

        </script>
</body>
</html>

<div> 태그를 두 개 만들어주고 아래에서 자바스크립트로 div태그 안에 들어갈 요소들을 만들어주고

버튼에는 이벤트를 만들어 주었다. 

red 쪽에 있는 버튼을 클릭하면 div두 개의 배경색이 빨간색으로 변하고 

blue 쪽에 있는 버튼을 클릭하면 div 두 개의 배경색이 파란색으로 변한다.

버튼을 클릭하면 배경색이 변한다.

 이 코드에서 div를 추가해서 yellow를 하나 더 만들어 준다고 가정하면

        function yellow(){
            document.querySelector("#yellow").innerHTML = `
            <div class="component">
                <h1>blue</h1>
                <button onclick="document.querySelector('#blue').style.backgroundColor='yellow'
                document.querySelector('#red').style.backgroundColor='yellow'
                 document.querySelector('#yellow').style.backgroundColor='yellow'
                ">버튼</button>    
            </div>
            `
        }

이런 함수를 하나 추가해줘야 하고 위의 redblue 함수도 yellow박스에 색이 들어갈 수 있도록

yellow div에 배경색을 주는 코드를 한 줄 추가해줘야 한다 이렇게 하나의 태그가 늘어날 때 다른 함수들도 다 변경되어야 하는 관련성이 있다.

 

redux를 사용하면 이런 관련성을 낮춰줄 수 있다.

 

withredux.html (redux를 사용)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.min.js"></script>
    <style>
        .component{
            border: 5px solid #000;
            padding:10px;
        }
    </style>
</head>
<body>
    <div id="red"></div>
    <div id="blue"></div>
    <script>
        //1.리듀서 함수 만들기
        function reducer(state,action){
            //state초기값을 위해 처음에 호출
            if(state === undefined){
                return {color:"white"}
            }
            let newState;
            if(action.type === "CHANGE_COLOR"){
                newState={...state , color:action.color}
                console.log(newState)
            }
            return newState;
        }
        // 2. redux스토어 생성하기.
        const store = Redux.createStore(reducer);
        
        function red(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#red").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>red</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'red'})">
                버튼</button>    
            </div>
            `
        }    
        function blue(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#blue").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>blue</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'blue'})">
                버튼</button>    
            </div>
            `
        }    

        //구독하기
        store.subscribe(red); //상태가업데이트될때 해당함수를 다시 호출
        store.subscribe(blue);
        red();
        blue()
    </script>
    
</body>
</html>

이렇게 써주면 아까 리덕스를 쓰지 않고 만들었던 코드와 똑같은 기능을 할 수 있게 만들어졌다.

여기서는 새로운 div를 추가해 줘도 다른 함수를 수정해주지 않아도 된다.

 

yellow div 추가

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.2.0/redux.min.js"></script>
    <style>
        .component{
            border: 5px solid #000;
            padding:10px;
        }
    </style>
</head>
<body>
    <div id="red"></div>
    <div id="blue"></div>
    <div id="yellow"></div>
    <script>
        //1.리듀서 함수 만들기
        function reducer(state,action){
            //state초기값을 위해 처음에 호출
            if(state === undefined){
                return {color:"white"}
            }
            let newState;
            if(action.type === "CHANGE_COLOR"){
                newState={...state , color:action.color}
                //  newState = Object.assign({},state,{color:action.color}) // object.assign 객체 복사 ({복사될대상},복사할객체,추가할내용)
                console.log(newState)
            }
            return newState;
        }
        // 2. redux스토어 생성하기.
        const store = Redux.createStore(reducer);
        
        function red(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#red").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>red</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'red'})">
                버튼</button>    
            </div>
            `
        }    
        function blue(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#blue").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>blue</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'blue'})">
                버튼</button>    
            </div>
            `
        }    
        function yellow(){
            let state = store.getState();// 상태값 반환
            document.querySelector("#yellow").innerHTML = `
            <div class="component" style="background-color:${state.color}">
                <h1>yellow</h1>
                <button onclick="store.dispatch({type:'CHANGE_COLOR',color:'yellow'})">
                버튼</button>    
            </div>
            `
        }    



        //구독하기
        store.subscribe(red); //상태가업데이트될때 해당함수를 다시 호출
        store.subscribe(blue);
        store.subscribe(yellow);
        red();
        blue();
        yellow();
    </script>
    
</body>
</html>

 

 

 

이제 리액트에서 redux를 사용해 보자

 

React에서 redux사용하기

 

redux, react-redux 설치하기

npm install redux
npm install react-redux

 

redux사용법 

스토어 생성하기

//createStore 불러오기
import { legacy_createStore as createStore } from 'redux';
//store 만들기
const store = createStore(리듀서함수)

 

스토어 사용하기

스토어를 사용하려면 상위컴포넌트에서 Provider로 감싸줘야 하위 컴포넌트에서 store를 불러서 쓸 수 있다.

import { Provider } from 'react-redux';

<Provider store={store}>
   <App />
</Provider>

 

* 리덕스 스토어 상태 조회하는 Hook함수 useSelector

import { useSelector } from 'react-redux";

const state = useSelector(state=>state)

* 리덕스 스토어 디스패치 반환하는 Hook함수 useDispatch

import { useDispatch } from 'react-redux";

const dispatch = useDispatch()

​이렇게 어디서든 디스패치와 상태값을 불러서 사용할 수 있다.

 

예제 만들기

리덕스 모듈 생성 후 스토어 생성하기

리덕스 모듈이란 액션 타입, 액션생성함수, 리듀서 가 모두 들어있는 자바스크립트 파일.

여러 개의 리덕스 모듈을 생성하여 하나의 리덕스 모듈로 합쳐서 사용한다.

* combineReducers({ 리덕스모듈 객체 }) 함수를 사용하여 리덕스 모듈을 합쳐줌.

import { combineReducers } from "redux";
const rootReducer = combineReducers({
    리덕스모듈1: 리덕스모듈1,
    리덕스모듈2: 리덕스모듈2
}) ---> 리듀서를 합쳐줌

counter 모듈 + todos 모듈-----> 루트리듀서 ----> 스토어 생성.

 

주의할 점

액션 타입 - 다른 리덕스 모듈과 겹치지 않게 하기 위해 컴포넌트 이름의 접두사를 앞에 붙여줘야 한다

counter/SET_DIFF

액션 생성함수 -액션객체를 리턴해주는 함수. (export로 내보내기)

리듀서 - 컴포넌트 이름으로 만들어지며 state 파라미터에 디폴트 초기값을 지정합니다.

(export default로 내보내기)

state=initialState

 

모듈들을 관리해 줄 modules폴더를 만들고 그 안에 모듈들을 만들어준다.

 

modules/counter.js

//액션 : 타입 만들기
const SET_DIFF =  "counter/SET_DIFF";
const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";

//액션 생성 함수 만들기
export const setDIFF = (diff)=>({type:SET_DIFF,diff});
export const increase = () =>({type:INCREMENT});
export const decrease = () =>({type:DECREMENT});
//초기 상태 선언
const initialState = {
    number: 0 ,
    diff: 1
}
//리듀서 선언
export default function counter(state=initialState , action){
    switch(action.type){
        case SET_DIFF:
            return{
                ...state,
                diff:action.diff
            }
        case INCREMENT:
            return{
                ...state,
                number:state.number + state.diff
            }
        case DECREMENT:
            return{
                ...state,
                number:state.number - state.diff
            }
        default:
            return state;
    }
}

 

modules/todos.js

//액션타입선언
const ADD_TODO = 'todos/ADD_TODO';
const TOGGLE_TODO = 'todos/TOGGLE_TODO';

//액션 생성 함수
let nextId = 1
export const addTodo = (text) =>({
    type:ADD_TODO,
    todo:{id:nextId,text: text,done:false}
})
export const toggleTodo = (id)=>({
    type:TOGGLE_TODO,
    id:id
})

//초기상태값
//초기 상태는 배열이어도 되고,원시타입(숫자,불린,문자열) 객체도 가능하다.
const initialState = [
    // {
    //     id:1,
    //     text:"해야할일",
    //     done:false
    // }
]
//리듀서

export default function todos(state=initialState,action){
    switch(action.type){
        case ADD_TODO:
            return[
                ...state,
                action.todo
            ]
        case TOGGLE_TODO:
            return state.map(todo => todo.id === action.id ? {...todo, done:!todo.done} : todo )
        default:
            return state
    }
}

 

​modules/index.js  rootreducer 만들기

import { combineReducers } from "redux";
import counter from "./counter";
import todos from "./todos";

//한프로젝트에 리듀서가 여러개일때 하나로 합쳐서 사용 => rootreducer 루트리듀서
//combinereducers( )

const rootReducer = combineReducers(
    {
        counter:counter,
        todos:todos
    }
    )
export default rootReducer

 

스토어 생성하기

src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { legacy_createStore as createStore } from 'redux';
import rootReducer from './modules';
import { Provider } from 'react-redux';
import { devToolsEnhancer } from '@redux-devtools/extension'; // 크롬확장 redux devtools
//스토어 만들기
const store = createStore(rootReducer,devToolsEnhancer());

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

// 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();

 

store를 만들고 Provider로 묶어줘야 하위 컴포넌트에서 store요소를 불러서 사용할 수 있다.

이제 이 값들을 불러서 화면을 구성하고 상태값을 변경해 줄 컴포넌트를 만들어주면 된다.

 

컴포넌트 만들기


1. 프레젠테이셔널 컴포넌트 

실제 보이는 태그들을 만들어주는 컴포넌트, redux스토어에 직접 접근하지 않음
필요한 값은 props로 받아와서 사용하는 컴포넌트, 주로 UI를 선언하는 것에 집중

 

2. 컨테이너 컴포넌트

 

프레젠테이셔널 컴포넌트를 감싸고 store에 접근해서 값을 props로 프레젠테이셔널로 전달.
리덕스 스토어의 상태를 조회하거나, 액션을 디스패치 할 수 있는 컴포넌트.
프리젠테이셔널 컴포넌트를 불러와서 사용

 

꼭 이렇게 만들어줄 필요는 없지만 기능적으로 분리해 주면 나중에 에러가 났을 때 찾기 편하다

 

1. 프레젠테이셔널 컴포넌트

components/Counter.js

import React from 'react';

const Counter = ({number,diff,onIncrease,onDecrease,onsetDIFF}) => {
    const onChange = (e)=>{
        onsetDIFF(Number(e.target.value)) //(diff)=>({type:SET_DIFF,diff});
    }
    return (
        <div>
            <h1>{number}</h1>
            <div>
                <input type="number" value={diff} min="1" onChange={onChange}/>
                <button onClick={onIncrease}>+</button>
                <button onClick={onDecrease}>-</button>

            </div>
        </div>
    );
};

export default Counter;

 화면을 구성해 주는 요소들이 들어있고 필요한 값들은 props로 컨테이너 컴포넌트한테서 받아온다.

 

2. 컨테이너 컴포넌트

containers/CounterContainer.js

import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Counter from '../components/Counter';
import { decrease, increase,setDIFF } from '../modules/counter';

const CounterContainer = () => {
    //useSelector() 는 리덕스스토어의 상태를 조회하는 hook이다

    const { number,diff } = useSelector(state=>state.counter);
    const dispatch = useDispatch();
    // 각 액션들을 디스패치 하는 함수
    const onIncrease = ()=> dispatch(increase());
    const onDecrease = ()=>dispatch(decrease());
    const onsetDIFF = (diff) => dispatch(setDIFF(diff));
    return (
        <Counter number={number} diff={diff} onIncrease={onIncrease} onDecrease={onDecrease} onsetDIFF={onsetDIFF}/>
    );
};

export default CounterContainer;

 

컨테이너 컴포넌트에서 Counter컴포넌트를 불러서 사용하고 Props로

함수=dispatch(액션생성함수)와 상태값을 보내주고 있다.

 

App.js에서 CounterContainer 컴포넌트를 불러서 사용하면 끝

 

redux자체는 그렇게 되게 어렵지는 않은데 이번에 redux를 배우면서 새로운 개념들을 같이 배워서 이해하기가 정말 너무 힘들었다. 모듈을 다 나눠서 만들고 합쳐서 하나로 만들고 액션객체도 일일이 만들어줬었는데 이번에는 액션객체를 만드는 함수를 만들어서 사용하니 갑자기 연결된 애들이 많아져서 뭐가 어디서 불러서 쓰고 어디에 들어간게 맞는건지 구분을 못해서 수업시간에 좀 힘들었다.. 그래도 redux는 많이 사용하고 있다고하니 연습해서 꼭 잘 쓰고싶다.. 포기하지말자.. 수료 2달남았다.. ㅠㅠ

반응형
반응형

오늘은 프로젝트에 상품을 추가할 수 있는 기능을 배웠다. 아직 적용은 안 됐지만 나중에 상품을 추가하려면 이미지나 파일을 보내는 기능이 필요하다. 0.2 버전을 업데이트해보자!

이번에도 역시 서버와 데이터베이스가 연동되어서 실행되기 때문에 코드와 이미지로만 올리겠다.

(이거진짜 코드 샌드박스로 안되는건가..) 

 

0.1 버전에서는 상품이미지를 클릭해서 들어가는 ProductPage에서 url파라미터를 useParams로 받아서

이미지 좌표만 바뀌게 해 줬는데 이번에는 클릭하고 들어갔을 때 url파라미터를 서버에서 받아서

특정데이터를 조회해서 응답으로 보내주게 만들어준다.

 

 

서버 측 index.js

app.get("/products/:id",(req,res)=>{
    const params = req.params;
    const {id} = params;
    conn.query(`select * from products where p_id=${id}`,function(error,result,fields){
        res.send(result); //url에 들어간 번호파라미터로 mysql레코드
    });
})

요청한 주소의 params가 req에 담기게 되고 그 값을 id에 할당해서 select 문의 where 조건절에

조건으로 넣어주었다. 

sql에서 조회된 결과는 res.send로  응답데이터로 클라이언트에게 응답해 준다.

 

Product / index.js

import axios from 'axios';
import React from 'react';
import { useParams } from 'react-router-dom';
import useAsync from '../customHook/useAsync';
import './index.css'



async function productFetch(id){
    const response = await axios.get(`http://localhost:8080/products/${id}`);
    return response.data
}

const ProductPage = () => {
    const {p_id} = useParams();
    const state = useAsync(()=>productFetch(p_id),[]);
    const {loading,error,data} = state;
    if (loading) return <div>로딩중</div>
    if (error) return <div>에러발생</div>
    if (!data) return null
    const [Product] = data
    return (
        <div className='productDetail'>
            <h2>기초스킨케어 세트</h2>
            <div className='productImg'>
                <img src={`../${Product.p_img}`} alt="" />
            </div>
            <div>
                <p>{Product.p_desc}</p>
                <p>{Product.p_name}</p>
                <p>가격 : {Product.p_price}</p>
                <p>무료배송</p>
            </div>
        </div>
    );
};

export default ProductPage;

product폴더에 있는 index.js폴더에서  axios로 서버에 product경로로 get요청을 보내고

useAsync 커스텀훅에서 처리해서 state를 반환 해준다.

받아온 state에서 data로 상품이름과 이미지 좌표 가격을 띄워준다. 

(받아온 데이터는 배열의 형태로 저장되어 있어서 배열의 인덱스로 사용하거나

배열 구조분해할당으로 변수에 할당해서 사용해야 한다 )

[{p_id: 4, p_name: '기초스킨케어4', p_price: 70000, p_desc: '인기있는 상품 입니다.', p_quantity: 60, …}]

이런 형식으로 저장되어 있어서 data에서 바로 값으로 접근할 수는 없다.  data[0].p_id / Product.p_id

데이터베이스에서 받아온 값으로 화면 띄우기

 

제품 등록하기 )

 

제품을 등록하기 위해서는 클라이언트에서 보내주는 제품정보를 서버에서 받아서

데이터베이스에 저장해 주는 과정이 필요하다.

제품정보는 Post요청 으로 클라이언트에서 보내주고 Server에서도 Post로 받아서 

데이터베이스에 저장한다.

 

서버 측에 Post를 받는 구문을 추가해 준다.

Node.js > index.js

1
2
3
4
5
6
7
8
app.post("/addProduct",async (req,res)=>//req 가 가고 res 가 바로오는게 아니기때문에 async 붙여줘야함
    const {p_name,p_price,p_desc,p_img,p_quantity} = req.body;
    conn.query("insert into products (p_name,p_price,p_desc,p_img,p_quantity) values(?,?,?,?,?)",
    [p_name,p_price,p_desc,p_img,p_quantity],
    (error,result,fields)=>{
        res.send("ok");
    })
})
cs

req.body에 클라이언트가 보내주는 데이터가 담겨있다. 구조분해 할당으로  각 변수에 할당해 준다.

데이터베이스에 insert문으로 받아온 데이터를 추가해 준다.

 

쿼리문 작성형식

conn.query("쿼리문insert value(?,?,?)",[들어갈값],함수()=>{})

 

데이터베이스에 정상적으로 데이터가 입력되면 응답으로 ok를 보내준다.

인풋태그에 데이터를 모두 입력하고 form이 submit 될 때 데이터를 addProduct경로로

보내주도록 만들어준다.

 

클라이언트 측 

upload / index.js

import axios from 'axios';
import React,{useState} from 'react';
import { useNavigate } from 'react-router-dom';
import './index.css'


const UploadPage = () => {
    const navigate=useNavigate()
    const [ formData,setFormData] = useState({
        p_name:"",
        p_price:"",
        p_img:"",
        p_desc:"",
        p_quantity:""
    })
    const onChange = (e)=>{
        const {name,value} = e.target;
        setFormData({
            ...formData,
            [name]:value
        })
    }
    const onReset =()=>{
        setFormData({
            p_name:"",
            p_price:"",
            p_img:"",
            p_desc:"",
            p_quantity:""    
        })
    }
    const onSubmit = (e)=>{
        //form에 연결된 이벤트를 제거
        e.preventDefault()
        // 입력이 다 되어있는지 체크
        if(formData.p_name && formData.p_price && formData.p_img && formData.p_desc && formData.p_quantity){
            insertProduct();
        }
    }
    function insertProduct(){
        // 서버에 post요청
        axios.post("http://localhost:8080/addProduct",formData)
        .then(res=>{
            console.log(res); //콘솔에 응답출력
            navigate('/'); //메인화면으로 이동
        })
        .catch(e=>{
            console.log(e)
        })
    }
    return (
        <div className='upload'>
            <h2>제품등록하기</h2>
            <form onSubmit={onSubmit}>
                <table>
                    <thead></thead>
                    <tbody>
                    <tr>
                        <td>상품이름</td>
                        <td>
                            <input type="text" name="p_name" value={formData.p_name} onChange={onChange}/>
                            
                        </td>
                    </tr>
                    <tr>
                        <td>이미지</td>
                        <td>
                            <input type="text" name="p_img"  value={formData.p_img} onChange={onChange}/>
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품가격</td>
                        <td>
                            <input type="number" name="p_price" value={formData.p_price} onChange={onChange}/>
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품수량</td>
                        <td>
                            <input type="number" name="p_quantity" value={formData.p_quantity} onChange={onChange}/>
                        </td>
                    </tr>
                    <tr>
                        <td>상세설명</td>
                        <td>
                            <textarea name="p_desc" onChange={onChange} value={formData.p_desc}>
                                {formData.p_desc}
                            </textarea>
                        </td>
                    </tr>
                    <tr>
                        <td colSpan={2}>
                            <button type='submit'>등록</button>
                            <button type='reset' onClick={onReset}>취소</button>
                        </td>
                    </tr>
                   </tbody>
                    <tfoot></tfoot>
                </table>
            </form>
        </div>
    );
};

export default UploadPage ;

각 input태그의 value값을 state로 관리하고 값이 입력돼서 변경되면 onChange={onChange} 함수가 실행돼서 상태값이 변경되게 된다. 아래에 취소버튼을 누르면 상태값이 다 공백으로 바뀌는 onReset함수 도 만들어 줬다.

등록 버튼을 누르면 onSubmit이벤트가 실행되는데 기본적으로 실행되는 onSubmit이벤트가 아니고

따로 함수를 만들어줄 거 기 때문에 preventDefault( )로 기본적으로 실행되는 이벤트를 막아준다

인풋태그에 데이터가 전부 입력되어 있으면 insertProduct( ) 함수를 실행시켜 준다.

 

onSubmit

    const onSubmit = (e)=>{
        //form에 연결된 이벤트를 제거
        e.preventDefault()
        // 입력이 다 되어있는지 체크
        if(formData.p_name && formData.p_price && formData.p_img && formData.p_desc && formData.p_quantity){
            insertProduct();
        }
    }

 

insertProduction

    function insertProduct(){
        // 서버에 post요청
        axios.post("http://localhost:8080/addProduct",formData)
        .then(res=>{
            console.log(res); //콘솔에 응답출력
            navigate('/'); //메인화면으로 이동
        })
        .catch(e=>{
            console.log(e)
        })
    }

insertProduct 함수가 실행되면 axios.post로 경로에 입력된 상태값 데이터(formData)를 보내주게 되고.

응답을 받으면 콘솔에 응답을 출력해 주고 navigate로 홈화면으로 이동한다.

 

서버에 이미지 업로드하기)

이미지파일을 클라이언트가 가지고 있을게 아니라

서버에서 이미지파일을 가지고 있다가 보내줘야 하기 때문에 업로드 할때

이미지파일도 같이 업로드 해주어야 한다.

 

클라이언트부터 만들어주자. 아직 upload에 넣을게 아니고 테스트만 해보는 거라서

ImageForm.js를 components폴더 안에 만들어 주었다.

 

components/ImageForm.js

import axios from 'axios';
import React,{useState} from 'react';

const ImageForm = () => {
    const [imageURL,setimageURL] = useState(null)
    const onChangeImage = (e)=>{
        const{name} = e.target;
        //<form>태그생성
        const imageFormData = new FormData();
        //<form>태그에 속성 추가하기
        imageFormData.append(name,e.target.files[0]);
        axios.post('http://localhost:8080/upload',imageFormData,{
            Headers:{'content-type':'multipart/formdata'},
        }).then(res=>{
            console.log(res);
            setimageURL(res.data.imageURL);
        }).catch(e=>{
            console.log(e)
        })
    }
    return (
        <div>
            <table>
                <tr>
                    <td>file</td>
                    <td>
                        <input type="file" name="file" encType="multipart/form-data" onChange={onChangeImage}/>
                        {
                            imageURL ? <img src={`./images/${imageURL}`} alt="" width='200px' height='200px'/>:
                            (<div id="upload-img-bg">
                                <img src="images/cameraicons.png" alt="" width='200px' height='200px'/>
                            </div>)
                        }
                    </td>
                </tr>
            </table>
        </div>
    );
};

export default ImageForm;

 파일선택 버튼을 누르고 이미지 파일을 넣으면 onChange 이벤트가 실행되면서

onChangeImage함수가 실행된다.

    const onChangeImage = (e)=>{
        const{name} = e.target; //파일형식을 받음
        //<form>태그생성
        const imageFormData = new FormData();
        //<form>태그에 속성 추가하기
        imageFormData.append(name,e.target.files[0]);
        axios.post('http://localhost:8080/upload',imageFormData,{
            Headers:{'content-type':'multipart/formdata'},
        }).then(res=>{
            console.log(res);
            setimageURL(res.data.imageURL);
        }).catch(e=>{
            console.log(e)
        })
    }

input태그에서 바로 전송을 해주기 때문에 Form이 없어서

Form을 만들어서 거기에 데이터를 넣어서 보내준다.

        const imageFormData = new FormData();
        //<form>태그에 속성 추가하기
        imageFormData.append(name,e.target.files[0]);

이미지를 axios.post로 보내주고 Header에 콘텐츠의 타입을 정의해 준다. 이미지를 보내면

서버에서 응답으로 {imageURL:파일이름}을 보내준다.

응답을 받아서 이미지태그에 응답으로 받은 이미지이름 넣어준다.

{
      imageURL ? <img src={`./images/${imageURL}`} alt="" width='200px' height='200px'/>:
      (<div id="upload-img-bg">
         <img src="images/cameraicons.png" alt="" width='200px' height='200px'/>
      </div>)
}

 

이제 서버 측도 수정해줘야 한다 파일을 받아주려면 multer API를 설치해줘야 한다

 

GitHub - expressjs/multer: Node.js middleware for handling `multipart/form-data`.

Node.js middleware for handling `multipart/form-data`. - GitHub - expressjs/multer: Node.js middleware for handling `multipart/form-data`.

github.com

 

multer 설치하기

npm install --save multer

multer 불러오기

const multer = require("multer");

 

storage 생성하기

//diskstorage()--> 파일을 저장할때에 모든 제어 기능을 제공
const storage = multer.diskStorage({
    destination:(req,file,cb)=>{
        cb(null,'upload/');
    },
    filename:(req,file,cb)=>{
        const newFilename = file.originalname;
        cb(null,newFilename);
    }

})

파일을 저장할 경로와  저장할 파일의 이름을 설정해 줄 수 있다.

저장경로는 upload폴더 이고 

파일이름은 파일의 원래이름값인 file.originalname으로 해주었다.

 

upload객체 생성하기

const upload = multer({ storage : storage }); // {dest:storage} => {storage:storage}
app.post('/upload',upload.single('file'),async (req,res)=>{
    res.send({
        imageURL:req.file.filename
    })
});

이제 upload경로로 Post요청을 보내면 upload객체가 실행되면서 파일을 받아서 서버에서 저장해 준다.

 

클라이언트에서 파일을 올리면 서버로 보내져서 upload폴더에 저장되고 서버에서 응답으로 이미지의 이름을 보내주게 되고 클라이언트는 받은 이름으로 클라이언트자신의 images폴더에 접근해서 해당하는 이미지를 띄워준다.

일단은 테스트기 때문에 css도 없고 사실 파일도 클라이언트의 img폴더가 아니라 클라이언트가 이미지도 보내줘야하는 게 아닌가 싶긴한데 내일 학원가면 이어서 마저 가르쳐주실테니 일단 여기서 만족한다 multer를 쓰는방법은 링크의 공식문서를 읽어보면 있으니 찾아서 하면될것같고 요즘 하면서 점점느끼는게 외우는 걸 포기하게 되는거다.. ㅋㅋㅋ 어떻게 다외우나 싶어서 선생님께 여쭤봤더니 외우면서 하는게 아니라 그때그때 잘 찾아서 써야 한다고 한다. 이제 외우려고 노력하지 말고 열심히 블로그에 기록하고 다른 블로그에 좋은 포스팅이 있으면 저장해 뒀다가 잘 베껴서 써야겠다 코딩은 정말 엄청 두꺼운 책으로 오픈북 시험을 치는 느낌이다.. ㅠ

반응형
반응형

화장품을 파는 쇼핑몰 사이트를 만들어보자!!

메인페이지, 상품페이지, 상품업로드 페이지를 만들고 각 페이지는 폴더를 따로 만들어서 저장한다.

컴포넌트폴더를 만들고  HeaderFooter (상단화면 하단화면)을 만들어 준다.

메인페이지에 들어갈 상품태그도 컴포넌트로 만들어서 컴포넌트 폴더에 넣어주겠다.

 

코드샌드박스로 코드를 올릴 텐데 서버는 에러가뜨고 로컬에 있는 mysql이랑 어떻게 연결하는지 몰라서 리액트만 코드샌드박스로 올리고 서버와 mysql연결하는 부분은 코드로 올리겠다..

 

우선 headerfooter main page를 만들어준다

 

Header.js /Header.css

import React from 'react';
import { Link } from 'react-router-dom';
import './Header.css';
const Header = () => {
    return (
        <div className='header'>
            <h1><Link to='/'>Cosmeticsv</Link></h1>
            <ul>
            <li>스킨케어</li>
            <li>메이크업</li>
            <li><Link to='/upload'>제품등록</Link></li>
            </ul>
        </div>
    );
};
export default Header;

-----------------------------------------------------------------------------------------------

Header.css
a{
    text-decoration: none;
    color: inherit;
}
.header{
    display: flex;
    width: 100%;
    max-width: 1200px;
    padding: 20px;
    justify-content: space-between;
    margin: 0 auto;
}
.header ul{
    display: flex;
}
.header li{ padding:0 20px; list-style: none; }

Header에서는 제목을 클릭하면 메인화면으로 빠져나갈 수 있게 Link를 넣어주었고 제품등록을 클릭하면 제품등록창으로 이동할 수 있게 Link를 넣어주었다.

 

Footer.js /Footer.js

import React from 'react';
import './Footer.css'

const Footer = () => {
    return (
        <div className='footer'>
            <div className='info'>
                <div className='inner'>
                    <div >
                        <h2>무통장입금계좌</h2>
                        <div>
                            <p>BANK ACCOUNT</p>
                            <p>301-1234-5678-01</p>
                            <p>예금주 - 김혜라</p>
                        </div>
                    </div>
                    <div>
                        <h2>고객센터</h2>
                        <div>
                            <p>영업시간 이외에는 문의 게시판을 이용해주시면 당담자 확인 후 빠른 답변 도와드리겠습니다.</p>
                            <p>02-1263-1245</p>
                        </div>
                    </div>
                    <div>
                        <h2>공지사항</h2>
                        <ul>
                            <li>조명가이드 2022-06-20</li>
                            <li>신상품 입고 안내 2022-06-10</li>
                            <li>몰 오픈을 축하드립니다. 2022-02-20</li>
                        </ul>
                    </div>
                </div>
            </div>
            <div className='footermenu'>
                <div className='inner'>
                    <ul>
                        <li>홈</li>
                        <li>매장안내</li>
                        <li>이용약관</li>
                        <li>개인정보처리방침</li>
                    </ul>
                </div>
            </div>
            <div className='address'>
                <div className='inner'>
                상호 :  주소 : 울산광역시 남구 어딘가 대표전화 : 국번없이 123-456-7891 대표이사 : 김OO 개인정보관리자 : 이OO 사업자 등록번호 : 102-12-12345 copyright(c) Greck Lamp,.LTD all rights reserved.
                </div>
            </div>
        </div>
    );
};

export default Footer;
----------------------------------------------------------------------------------------------------------------------
Footer.css

.footer {
    margin-top: 60px;
    text-align: left;
}
.info{
    border-top:3px solid #333;
    border-bottom:1px solid #333;
    padding: 20px 0;
}
.inner{
    width:100%;
    max-width:1200px;
    margin: 0 auto;
}
.info .inner{
    display: flex;
    justify-content: space-between;
}
.info .inner > div{
    width: 32%;
    padding-bottom:20px ;
    line-height: 1.8;
}
.info .inner h2{
    border-bottom: 1px solid #ccc;
    line-height: 50px;
    font-size: 20px;
}

.info .inner > div div{
    padding:20px 0 ;
}
.info .inner > div ul {
    padding: 20px 0;
}
.footermenu ul{
    display: flex;
    height:60px;
    align-items: center;
}
.footermenu ul li{
    padding: 0 20px;
    border-right: 1px solid #ccc;
}
.footermenu ul li:last-child{
    border-right:none;
}
.address{
    border-top:1px solid #ccc;
    background-color: #eee;
    padding: 30px 0;
}

 

Footer는 좀 긴데 일반 사이트처럼 별로 기능은 들어간게없고 그냥 정보를 알려주는 용도이다. 

 

ProductList.js

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

const ProductList = ({p_id,p_price,p_name}) => {
    return (
        <li>
            <Link to={`/detailView/${p_id}`}>
                <img src={`../images/cosmetic${p_id}.JPG`} alt="" />
                <h3>{p_name}</h3>
                <p>{p_price}원</p>
                <p>간단한 설명입니다.</p>
            </Link>
        </li>
    );
};

export default ProductList;

ProductList는 메인페이지 의 ul태그에 넣어줄 컴포넌트이다 같은 li태그 구조가 반복되기 때문에

컴포넌트로 만들고 달라지는 이미지와 상품명 가격은 props로 받아서 넣어준다.

 

HeaderFootercomponents폴더에 넣어서 보관해준다.

 

index.js / index.css (src/main폴더)

import React from 'react';
import { Link } from 'react-router-dom';
import ProductList from '../components/ProductList';
import './index.css';

const data=[ //이 데이터를 서버에서 받아올거임
	{
		p_id:"1",
    	p_name:"상품1",
        p_price:"10000",
    },
    {
		p_id:"2",
    	p_name:"상품2",
        p_price:"20000",
    },
    {
		p_id:"3",
    	p_name:"상품3",
        p_price:"30000",
    },
    {
		p_id:"4",
    	p_name:"상품4",
        p_price:"40000",
    },
]

const MainPage = () => {    
    return (
        <div className='main'>
            <div className='visual'>
                <img src ="images/banner1.jpg" alt="배너이미지1"/> 
            </div>
            <div className='product'>
                <h2>신상품</h2>
                <ul>
                    {data.map(pro=>
                    <ProductList key={pro.p_id} p_id={pro.p_id} p_name={pro.p_name} p_price={pro.p_price}/>)}
                </ul>
            </div>
        </div>
    );
};

export default MainPage;

----------------------------------------------------------------------------------------------------------
index.css

.visual img{
    width: 100%;
}
.product{
    width:100%;
    max-width: 1200px;
    margin: 0 auto;
}
.product h2{
    padding-bottom:40px
}
.product ul {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
}
.product ul li{
    width:25%;
    /* border: 1px solid #ddd; */
    padding-bottom: 20px;
}
.product ul li img{
    width: 100%;
}
.product ul li h3{
    padding: 10px;
}
.product ul li p {
    padding: 0 10px;
}

 

 

원래 mainpage에 들어갈 이미지는 mysql에서 받아와야 하는데 코드샌드박스로는 할 수 없기 때문에

data 객체 를 따로 코드 안에 만들어 주었다. 

 

이제 상품이미지를 클릭했을 때 상품상세화면으로 들어가게 해 주겠다. 

ProductList.js에서 이미지를 클릭할 때 이동할 링크를 '/detailView/${p_id}'로 줬기 때문에

App에서도 <Route path = '/detailView/:p_id' element=(<ProductPage/>)/>를 넣어서 받아준다

function App() {
  return (
    <div className="App">
      <Header/>
      <Routes>
        <Route path='/' element={<MainPage/>}/>
        <Route path='/detailView/:p_id' element={<ProductPage/>}/>
      </Routes>
      <Footer/>
    </div>
  );
}

 

이제 ProductPage를 만들어보자

 

index.js /index.css (src / product폴더)

import React from 'react';
import { useParams } from 'react-router-dom';
import './index.css'

const ProductPage = () => {
    const {p_id} = useParams();

    return (
        <div className='productDetail'>
            <h2>기초스킨케어 세트</h2>
            <div className='productImg'>
                <img src={`../images/cosmetic${p_id}.JPG`} alt="" />
            </div>
            <div>
                <p>스킨케어 주간 베스트</p>
                <p>헤라 스킨</p>
                <p>가격 : 65,000</p>
                <p>무료배송</p>
            </div>
        </div>
    );
};

export default ProductPage;
-------------------------------------------------------------------------------------------------

.productDetail{
    margin: 0 auto;
    width:100%;
    max-width: 1200px;
}
.productDetail h2{
    padding-top: 30px;
    padding-bottom: 30px;
}
.productImg{
    text-align: center;
}
.productImg img{
    border-radius:50% ;
}

ProductPage에서는 ProductLists에서 Link에  /detailView/${p_id}를 넣어준 것을

useParams( )로 객체로 받아서 값을 

p_id에 할당해 주고 이미지 src를 설정하는 데 사용한다.

 

상품을 추가해 주는 upload페이지도 만들어 보겠다.

 

index.js/index.css (src / upload폴더)

import React from 'react';
import './index.css'

const UploadPage = () => {
    return (
        <div className='upload'>
            <h2>제품등록하기</h2>
            <form>
                <table>
                    <thead></thead>
                    <tbody>
                    <tr>
                        <td>상품사진</td>
                        <td>
                            <input type="file" name="p_img" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품이름</td>
                        <td>
                            <input type="text" name="p_name" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품소개</td>
                        <td>
                            <input type="text" name="p_info" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품가격</td>
                        <td>
                            <input type="number" name="p_price" />
                            
                        </td>
                    </tr>
                    <tr>
                        <td>상품수량</td>
                        <td>
                            <input type="number" name="p_quantity" />
                        </td>
                    </tr>
                    <tr>
                        <td>상세설명</td>
                        <td>
                            <textarea name="p_desc" ></textarea>
                        </td>
                    </tr>
                    <tr>
                        <td colSpan={2}>
                            <button type='submit'>등록</button>
                            <button>취소</button>
                        </td>
                    </tr>
                   </tbody>
                    <tfoot></tfoot>
                </table>
            </form>
        </div>
    );
};

export default UploadPage ;

------------------------------------------------------------------------------------------------

.upload{
    margin: 40px auto;
    width: 100%;
    max-width:1200px;
}
.upload h2{
    font-size: 24px;
    padding-bottom:20px;
}
.upload table{
    border-collapse: collapse;
    width: 100%;
}
.upload td{
    border-bottom: 1px solid #ccc;
    padding: 16px;
}
.upload td:nth-child(1){
    width:20%;
}
.upload input[type='text'], .upload textarea{
    width:100%;
}

upload페이지는 아직 형태만 만들어놓고 아무 기능도 추가하지 않았다

나중에는 추가했을 때 서버로 데이터가 전송되도록 해줄 것 같다.

upload페이지로 이동하는 주소는 Header에서  <Linkto='/upload'> 제품등록 </Link>으로 주고 있다. 

 

App.js 

import './App.css';
import Header from './components/Header';
import Footer from './components/Footer';
import MainPage from './main';
import ProductPage from './product';
import { Routes,Route } from 'react-router-dom';
import UploadPage from './upload';

function App() {
  return (
    <div className="App">
      <Header/>
      <Routes>
        <Route path='/' element={<MainPage/>}/>
        <Route path='/detailView/:p_id' element={<ProductPage/>}/>
        <Route path='/upload' element={<UploadPage></UploadPage>}/>
      </Routes>
      <Footer/>
    </div>
  );
}

export default App;

App에서 일치하는 경로에 따라 Route들을 렌더링 해주고 있다. 

 

 

 

 

데이터를 서버에서 받아오기

 

서버코드

//express 서버 만들기
const express = require("express");//import express
const cors = require("cors");

//mysql부르기
const mysql = require("mysql");

//서버 생성 --> express( )호출
const app = express();
//프로세서의 주소 포트번호 지정
const port = 8080;
// JSON형식의 데이터를 처리할수 있도록 설정
app.use(express.json());
// 브라우저의 CORS 이슈를 막기 위해 사용하는 코드
app.use(cors());

//sql 연결선 만들기
const conn = mysql.createConnection({
    host:"localhost",
    port:'3306',
    user:"root",
    password:"1234",
    database:"shopping"
})
//sql 연결하기 
conn.connect();



// get요청시 응답 app.get(경로,콜백함수)
app.get('/products',(req,res)=>{
    conn.query('select * from products',function(error,result,fields){
        res.send(result);
    });
})


//서버구동
app.listen(port,()=>{
    console.log("서버가 돌아가고있습니다.")
})

(서버실행방법: 터미널에 node 서버이름.js 입력) 

서버에서 로컬에 있는 데이터베이스에 접근해서 데이터를 가져와서 응답으로 보내준다.

 

서버에서 보내주는 값은 사이트에서 axios로 받아온다

 

커스텀훅 

useAsync.js

import React, { useEffect, useReducer } from 'react';
//1.상태초기화
const initialState = {
    loading:false,
    data:null,
    error:null,
}
//2.리듀서 함수구현
//로딩중일때 상태
//데이터를 성공적으로 받았을때 상태
//에러일때 상태
function reducer(state,action){
    switch(action.type){
        case "LOADING":
            return{
                loading:true,
                data:null,
                error:null,
            }
        case "SUCCESS":
            return{
                loading:false,
                data:action.data,
                error:null,
            }
        case "ERROR":
            return{
                loading:false,
                data:null,
                error:action.error,
            }
        default:
            return state;
    }
}


const useAsync = (callback,deps=[]) => {
    const [state,dispatch] = useReducer(reducer,initialState);
    //데이터 요청 함수
    const fetchData = async ()=>{
        //로딩의 value를 true로 업데이트
        dispatch({
            type:"LOADING"
        });
        // try catch 에러 처리 구문 ,,데이터를 요청하는 과정은 에러발생할 확률이 높음
        try{
            //axios에서 받는 함수가 들어감 useAsync를 사용하는 컴포넌트에서 각각 다른 주소를 넣어서 보내줌
            const data = await callback(); 
            dispatch({
                type:"SUCCESS",
                data  //data : data 인데 같으니까 생략가능함
            })
        }
        catch(e){
            dispatch({
                type:"ERROR",
                error:e
            })
        }
    }
    useEffect(()=>{
        fetchData();
    },deps)
    return state;
};

export default useAsync;

데이터를 받았을 때 상태를 바꿔주는 커스텀 훅을 만들어주고 

 

main.js

import axios from 'axios';
import React from 'react';
import { Link } from 'react-router-dom';
import ProductList from '../components/ProductList';
import useAsync from '../customHook/useAsync'
import './index.css';

async function productFetch(){
    const response = await axios.get("http://localhost:8080/products");
    return response.data
}

const MainPage = () => {
    const state = useAsync(productFetch,[])
    const{loading,error,data}=state;
    if (loading) return <div>로딩중</div>
    if (error) return <div>에러발생</div>
    if (!data) return null
    console.log(data)
    return (
        <div className='main'>
            <div className='visual'>
                <img src ="images/banner1.jpg" alt="배너이미지1"/>
            </div>
            <div className='product'>
                <h2>신상품</h2>
                <ul>
                    {data.map(pro=>
                    <ProductList key={pro.p_id} p_id={pro.p_id} p_name={pro.p_name} p_price={pro.p_price}/>)}
                </ul>
            </div>
        </div>
    );
};

export default MainPage;

메인화면에서 useAsync를 import 해서 사용 useAsync에 파라미터로 입력한 주소의 데이터를 얻어서

리턴해주는 함수와 빈 배열을 넣어준다. 이렇게 넣어주면 useAsync의 콜백함수로 들어가서

주소에서 받아온 데이터가 data상태값에 들어가게 되고 useAsync에서 반환해 주는 state값을

Mainpage에서 받아서 이용할 수 있게 된다.

 

 

 

 

 

반응형

+ Recent posts