본문 바로가기
포스코x코딩온

[포스코x코딩온] 풀스택 부트캠프 12주차 정리 -2 React(Context API, Reduct)

by 김선지 2024. 1. 12.

Context

 

부모에서 생선된 props를 자손 요소에 전달하려면 (1 -> 5로) react의 특성상 1 -> 2 -> 3-> 4 -> 5로 전달을 할 수밖에 없다.

이 문제를 해결하기 위해 나온 react의 내장 기능이 context API다.

이걸 사용하면 1 -> 5로 바로 쏴줄 수 있다.

예를 들면 전역적으로 엑세스가 필요한 유저 로그인 정보 같은 곳이나 N대 자손에게만 보내준다던가 할 때 쓰이면 될 것 같다.

함수형 컴포넌트에서는 createContext(), useCotext()의 훅과 함께 쓰인다.

 

 

UserContext.js 파일 (createContext()를 통해 만들어진 Context를 export)

import { createContext } from "react";
export const UserContext = createContext(null);

 

UserProvider.js 컴포넌트 파일 (UserContext.js 파일을 통해 import한 UserContext를 통해 Provider로 value를 자손에게 넘겨준다.) 다만 인자에는 createContext를 통해 만들어진 context가 들어가야한다. 그래서 import를 해준다.

다음 Provider에서는 객체를 보냈다.

import { UserContext } from "./UserContext";
import { useState } from "react";
// children: props에서 나온 props에 모든 것을 받는 것
function UserProvider({children}) {
    // props 객체 형태의 children을 인자로 받아서 하위 요소로 삽입

    // defaultUser로 설정한 값 (name, setName)
    // 이름 변경할 수 있게 useState 사용
    console.log('createContent',UserContext)

    const [name, setName] = useState('선지훈');

    return ( <>
    <UserContext.Provider value={{name, setName}}> {children} </UserContext.Provider>
    
    </> );
}

export default UserProvider;

 

UserProfile.js 파일 (UserProvider.js의 자식 컴포넌트 파일) - 넘겨준 value는 useContext를 통해서 받을 수 있다.

다만 인자에는 createContext를 통해 만들어진 context가 들어가야한다. 그래서 import를 해준다

import { useContext } from "react";
import { UserContext } from "../context/UserContext";

function UserProfile() {
    // useContext 사용해서 context 값을 쓸 수 있음
    console.log('유즈컨텍스트', useContext(UserContext));
    const {name, setName} = useContext(UserContext);

    return ( <>
    <h2>사용자 프로필</h2>
    <p>이름 : {name} </p>
    <button onClick={() => setName('코디')}>이름 변경</button>
    </> );
}

export default UserProfile;

 

+ 실험하다가 children의 규칙을 알았다. children은 아들 요소까지만 해당된다.

다음 코드를 보면 Panel 컴포넌트의 title은 "welcome"으로 들어가 있는 것을 확인할 수 있다.

다만 children은 아들 요소인 h1태그 자체와 (context는 포함 x) Button 컴포넌트 *2 만 해당함을 알 수 있다.

why?)  h1 태그의 textContent는 h1태그의 children이니까!

그래서  3번째 스샷의 Button 컴포넌트와 같이 children을 textContent 부분에 넣어줘야한다.

(생각해보니까 children이라는 명칭 자체가 자식이었네..?)

children 등의 props를 보내주는 가상 DOM
props 설명해주는 dev tool
스크린샷 3


Reduct

위에서 만든 Context들을 묶어서 한데 모아 쉽게 볼 수 있게 만든 라이브러리가 reduct이다. (Reducer를 이용하는 게 useReducer를 이용한 문법인 것 같다.)

다만 외장 라이브러리기 때문에 install이 필요하다.

npm install redux react-redux @reduxjs/toolkit

redux는 Store, Action, Reducer, Dispatch로 이루어져있다.

Store

Store는 상태(state)가 관리되는 공간으로 한 프로젝트에는 하나의 스토어만 가진다. (그래서 하나의 index store를 통해서 여러 state를 관리할 수 있다.)

Store의 데이터는 컴포넌트에서 직접 조작하지 않고 state처럼 리듀서 함수 (like setState)를 이용해서 조작한다. (캡슐화 때문인듯.)

 

Action

상태에 변화가 필요할 때 바뀌는 type을 정해주는, 컴포넌트에서 store에 운반되는 데이터로 객체로 표현된다.

리듀서가 수행할 작업을 설명한다.

 

Dispatch

Action에 따라서 Action을 발생시키는 method이다. dispatch(action)과 같은 형태로 액션 객체를 파라미터로 넣어서 호출한다.

(해봤는데 type이 하나만 있어서 필요 없을거라고 생각해서 인자를 넣지 않았는데 이러면 에러가 발생하기 때문에 넣어주자.)

에러가 발생하지 않아도 reducer에 switch나 if 해서 type을 한번 거르지 않으면 이 reducer가 렌더링 되자마자 한번 실행된다. 왜인지는 모르겠다. action에 type을 꼭 넣는 것이 문법인 이유가 있는 것 같다.

ex) default가 true인 state이고 버튼 클릭 시 !state를 반환하는 reducer인데 한번 reducer가 실행되서 초기값이 false가 나오는 상황

 

Reducer

액션의 type에 따라 변화를 일으키는 함수를 나타낸다.

첫 매개변수는 현재 state값, 두번째 매개 변수는 Action 값을 받아서 변경된 새로운 state 객체를 반환해준다.

 

 

하나의 Store만 사용한다면 createStore를 이용하면 된다.

counterReducer.js 파일 (store)

// type을 바로 객체형으로 return해주는 함수
export const plus = () => ({type: 'PLUS'});
export const minus = () => ({type: 'MINUS'});


//reducer 첫 인자는 state, 두 번째 인자는 action으로 dispatch()함수의 인자이다
const counterReducer = (state = { number: 0 }, action) => {
    switch (action.type) {
    // return {} 형태를 통해 setState 해주듯이 객체값으로 반환해준다.
        case 'PLUS':
            return {number: state.number +1};
        case 'MINUS':
            return {number: state.number -1};
        case 'RESET':
            return {number: 0};
        default:
            return {number: state.number}
    }
}
//reducer 자체를 export 해서 counterStore의 인자에 넣어준다
export default conterReducer;

 

index.js (메인 폴더에 있는.)

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App1';
import { createStore } from 'redux';
import counterReducer from './store/counterReducer';
import App1 from './App1';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';

const root = ReactDOM.createRoot(document.getElementById('root'));

// 여기서 store를 createStore를 만들어서 Provider로 가져왔기 때문에 counterReducer에 있는 변수들을 이용할 수 있다.
// ex) useSelector에서 인자에 state를 넣을 수 있다.
//  createStore를 통해 store를 만들어서 인자에 reducer를 넣고 provider에 store를 묶어준다.
const store = createStore(counterReducer);

// 이렇게 넘겨줌
const store = configureStore({ reducer: rootReducer }, composeWithDevTools());
// const store = createStore(isVisible);

root.render(
  <React.StrictMode>
    <Provider store={store}>
    <App1 />
    </Provider>
  </React.StrictMode>
);

 

App1.js

import { useSelector, useDispatch } from "react-redux";
import { plus, minus } from "./store/counterReducer";

function App1() {
// index.js에서 Provider 안에 store={store}를 통해 쏴줘서 useSelector 사용 가능.
// state는 state 자체를 말하고 인자로 들어온 state에서 state.number라는 값을 number에 return
  const number = useSelector((state) => state.number);
  return (
    <div className="Box">
      <h1>React Redux Ex</h1>
      <h2>number : {number}</h2>
      <Box1></Box1>
    </div>
  );
}

const Box1 = () => {
  return (
    <div className="Box">
        <h2>Box1</h2>
      <Box2></Box2>
    </div>
  );
};

const Box2 = () => {
  return (
    <div className="Box">
        <h2>Box2</h2>
      <Box3></Box3>
    </div>
  );
};

const Box3 = () => {
  return (
    <div className="Box">
        <h2>Box3</h2>
      <Box4></Box4>
    </div>
  );
};

// state를 쓸 때는 useSelector, dispatch(set함수)를 쓸 때는 useDispatch 
const Box4 = () => {
    const number = useSelector((state) => state.counter.number);
    const dispatch = useDispatch();
  return (
    <div className="Box">
      <h2>box4 : {number}</h2>
      // 여기서 plus()는 {type: "PLUS"} 등을 반환하는 함수
      <button onClick={() => dispatch(plus())}>Plus</button>
      <button onClick={() => dispatch(minus())}>Minus</button>
    </div>
  );
};

export default App1;

 

여러 Reducer를 이용하고 싶다면  ConfigureStore를 이용하자.

 

counterReducer.js

// 나중에 컴포넌트에서 액션을 쉽게 발생시킬 수 있도록 만든 함수
// rootReducer를 사용하고 있기 때문에 type도 어디 reducer를 이용하고 있는지 알아보기 쉽게 
// 정해주자. 이건 강제는 아니다.
export const plus = () => {
    return ({type: 'counter/PLUS'})
}
export const minus = () => {
    return ({type: 'counter/MINUS'})
}

// 초기값 정의
const initialState = {
    number: 0,
};

// reducer 정의: action을 발생시키는 함수
// 아랫줄 state = 에는 여러 객체가 들어갈 수 있음
// 이렇게
// state = { number: 0, something: 0};
const counterReducer = (state = initialState, action) => {
    switch(action.type) {
        case 'counter/PLUS':
            return {number: state.number + 1};
        case 'counter/MINUS':
            return {number: state.number -1};
        default:
            return state;
    }
};

export default counterReducer;

 

index.js (Store폴더에 있는 reducer들의 index )

import { combineReducers } from "redux";
import counterReducer from "./counterReducer";
import isVisibleReducer from "./isVisibleReducer";

// 이렇게 rootReducer를 정의하는데 combineReducers 함수를 통해서
// 객체값으로 reducer들을 인자로 넣어준다.
const rootReducer = combineReducers({
  counter: counterReducer,
  isVisible: isVisibleReducer,
});

export default rootReducer;

 

index.js (메인 폴더에 있는 REACT의 index.js)

import React from 'react';
import ReactDOM from 'react-dom/client';
import App1 from './App1';
import { Provider, } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from './store';
// 이건 chrome 확장프로그램이다. 이거 있으면 쉽게 redux요소를 볼 수 있다. (install 필요)
import { composeWithDevTools } from 'redux-devtools-extension';

const root = ReactDOM.createRoot(document.getElementById('root'));

// 이렇게 넘겨줌, 두번째 인자는 선택.
const store = configureStore({ reducer: rootReducer }, composeWithDevTools());

root.render(
  <React.StrictMode>
    <Provider store={store}>
    <App1 />
    </Provider>
  </React.StrictMode>
);

 

App1.js

import { useSelector, useDispatch } from "react-redux";
import { plus, minus } from "./store/counterReducer";

function App1() {
// rootReducer를 사용하고 있기 때문에 state의 경로 분기가 counter와 isVisible로 나뉜다.
// 그중에서도 현재 counter 라는 reducer를 사용하고 있으니 이걸로 정의.
  const number = useSelector((state) => state.counter.number);
  return (
    <div className="Box">
      <h1>React Redux Ex</h1>
      <h2>number : {number}</h2>
      <Box1></Box1>
    </div>
  );
}

const Box1 = () => {
  return (
    <div className="Box">
        <h2>Box1</h2>
      <Box2></Box2>
    </div>
  );
};

const Box2 = () => {
  return (
    <div className="Box">
        <h2>Box2</h2>
      <Box3></Box3>
    </div>
  );
};

const Box3 = () => {
  return (
    <div className="Box">
        <h2>Box3</h2>
      <Box4></Box4>
    </div>
  );
};

const Box4 = () => {
    const number = useSelector((state) => state.counter.number);
    const dispatch = useDispatch();
  return (
    <div className="Box">
      <h2>box4 : {number}</h2>
      // plus, minus 함수는 {type: 'counter/PLUS'} 등을 return해주는 함수.
      <button onClick={() => dispatch(plus())}>Plus</button>
      <button onClick={() => dispatch(minus())}>Minus</button>
    </div>
  );
};

export default App1;