유블로그

[React] react-redux 에서 Hooks 사용하기 본문

React

[React] react-redux 에서 Hooks 사용하기

yujeong kang 2021. 4. 14. 18:40

www.youtube.com/playlist?list=PL9FpF_z-xR_F-nxvw-VDld5wcCzYmSnrW

 

react-redux 에서 Hooks 사용하기

 

www.youtube.com

위 강의와

리액트를 다루는 기술 - 김민준

책 참고하였음

github.com/velopert/learning-react

 

velopert/learning-react

[길벗] 리액트를 다루는 기술 서적에서 사용되는 코드. Contribute to velopert/learning-react development by creating an account on GitHub.

github.com

위의 링크도 참고하였음


 

** ducks 패턴 적용 **

- components

- containers

- modules

 

components 에는 뷰

containers 에는 데이터 연결

modules 에는 리듀서, store 등 로직 저장


리듀서 작성법

/modules/index.js

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

const rootReducer = combineReducers({
    counter
});

export default rootReducer;

 

/modules/counter.js

const INCREMENT = "counter/INCREMENT";
const DECREMENT = "counter/DECREMENT";

export const increment = () => ({ type: INCREMENT });
export const decrement = () => ({ type: DECREMENT });

const initialState = 0;

const counter = (state = initialState, action) => {
  switch (action.type) {
    case INCREMENT:
      return state + 1;
    case DECREMENT:
      return state - 1;
    default:
      return state;
  }
};

export default counter;

 

 

Hooks

- useState : 함수형 컨포넌트에서 상태를 관리하기 위한 Hook.

const Counter = () => {
  const [value, setValue] = useState(0);

  return (
    <div>
      <p>
        현재 카운터 값은 <b>{value}</b> 입니다.
      </p>
      <button onClick={() => setValue(value+1)}>+1</button>
      <button onClick={() => setValue(value-1)}>-1</button>
    </div>
  );
};

export default Counter;

value를 직접 변경하면 안 됨! setValue등 setter를 통하여 변경해야한다.


- useSelector : store에 있는 값을 들고올 수 있다.

/containers/Counter.js

import React from "react";
import { useSelector } from "react-redux";
import Counter from "../components/Counter";

const CounterContainer = () => {
  // dependencies 배열에 있는 값이 바뀔때 useSelector 다시 실행
  // 아무것도 없으면 한 번만 실행
  const counter = useSelector((state) => state.counter, []);
  
  // 여기에 useActions 로 리듀서 연결 하는데... 현재 useActions 없으므로 생략
  
  return <Counter />;
};

export default CounterContainer;

/components/Counter.js

import React from "react";

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

export default Counter;

- useEffect : 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook

useEffect(() => {
	console.log('마운트 될 때만 실행');
}, []);

useEffect(() => {
	console.log('name 값이 바뀔 때만 실행');
}, [name]);

useEffect(() => {
    return () => {
    	console.log('언마운트되기 전 그리고 업데이트되기 직전에 실행')
    };
});

useEffect(() => {
    return () => {
    	console.log('언마운트될 때만 실행')
    };
}, [name]);

본격적으로 todo 구현을 통해 hooks 를 알아보자~~!

 

/modules/index.js

루트리듀서에 todo 리듀서 추가

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

const rootReducer = combineReducers({
    counter,
    todos,
});

export default rootReducer;

/modules/todos.js

todo 관련 action들 등록하여 리듀서 만들기

const CHANGE_INPUT = "todos/CHANGE_INPUT";
const INSERT = "todos/INSERT";
const TOGGLE_CHECK = "todos/TOGGLE_CHECK";
const REMOVE = "todos/REMOVE";

let id = 0;

export const changeInput = (input) => ({ type: CHANGE_INPUT, payload: input });
export const insert = (text) => ({
  type: INSERT,
  payload: {
    id: ++id,
    text,
  },
});

export const toggleCheck = (id) => ({ type: TOGGLE_CHECK, payload: id });
export const remove = (id) => ({ type: REMOVE, payload: id });

const initialState = {
  input: "",
  todos: [],
};

const todos = (state = initialState, action) => {
  switch (action.type) {
    case CHANGE_INPUT:
      return {
        ...state,
        input: action.payload,
      };
    case INSERT:
      return {
        ...state,
        todos: state.todos.concat({
          ...action.payload,
          done: false,
        }),
      };
    case TOGGLE_CHECK:
      return {
        ...state,
        todos: state.todos.map((todo) =>
          todo.id === action.payload ? { ...todo, done: !todo.done } : todo
        ),
      };
    case REMOVE:
      return {
        ...state,
        todos: state.todos.filter((todo) => todo.id !== action.payload),
      };
    default:
      return state;
  }
};

export default todos;

/components/TodoList.js

React.memo 로 재렌더링 방지하여 최적화하기

import React from "react";

const TodoItem = React.memo(({ todo, onRemove, onToggle }) => {
  const { id, text, done } = todo;
  return (
    <li
      style={{
        textDecoration: done ? "line-through" : "none",
      }}
    >
      <span onClick={() => onToggle(id)}>{text}</span>{" "}
      <button onClick={() => onRemove(id)}>삭제</button>
    </li>
  );
});

// React.memo 로 감싸주면 컴포넌트가 받은 props(여기선 todos, onRemove, onToggle)가
// 바뀌었을 때만 새로 렌더링한다.
// 즉, 아래의 input에서 value가 바뀌어 onChange 함수가 작동해도
// TodoItems가 불필요하게 재렌더링 되는 현상을 막아 최적화할 수 있다!!
const TodoItems = React.memo(({ todos, onRemove, onToggle }) => {
  return todos.map((todo) => (
    <TodoItem
      todo={todo}
      key={todo.id}
      onRemove={onRemove}
      onToggle={onToggle}
    />
  ));
});

const TodoList = ({ todos, input, onRemove, onToggle, onChange, onSubmit }) => {
  return (
    <div>
      <form onSubmit={onSubmit}>
        <input onChange={onChange} value={input} />
        <button type="submit">추가</button>
      </form>
      <ul>
        <TodoItems todos={todos} onRemove={onRemove} onToggle={onToggle} />
      </ul>
    </div>
  );
};

export default TodoList;

 

/containers/TodoList.js

useCallback, useDispatch 사용

 

 


- useReducer : useState보다 다양한 상태로 변경할 수 있는 Hook

리듀서는 현재상태와 액션을 전달받아 새로운 상태를 반환하는 함수다.

useReducer를 쓰면 컴포넌트 업데이트 로직을 컴포넌트 바깥으로 뺄 수 있어 좋다.

import React, { useReducer } from 'react';

function reducer(state, action) {
  // action.type 에 따라 다른 작업 수행
  switch (action.type) {
    case 'INCREMENT':
      return { value: state.value + 1 };
    case 'DECREMENT':
      return { value: state.value - 1 };
    default:
      // 아무것도 해당되지 않을 때 기존 상태 반환
      return state;
  }
}

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, { value: 0 });

  return (
    <div>
      <p>
        현재 카운터 값은 <b>{state.value}</b> 입니다.
      </p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-1</button>
    </div>
  );
};

export default Counter;

아래처럼 리듀서 사용가능하다. state가 너무 많아질경우 유용한 방법이라고 한다.

사실 아직 와닿진 않는다.

좀 더 봐야할 듯!!

import React, { useReducer } from "react";

function reducer(state, action) {
  return {
    ...state,
    [action.name]: action.value,
  };
}

const Info = () => {
  const [state, dispatch] = useReducer(reducer, {
    name: "",
    nickname: "",
  });
  const { name, nickname } = state;
  const onChange = (e) => {
    dispatch(e.target);
  };

  return (
    <div>
      <div>
        <input name="name" value={name} onChange={onChange} />
        <input name="nickname" value={nickname} onChange={onChange} />
      </div>
      <div>
        <div>
          <b>이름: </b> {name}
        </div>
        <div>
          <b>닉네임: </b> {nickname}
        </div>
      </div>
    </div>
  );
};

export default Info;

- useMemo : 함수형 컴포넌트 내부 연산을 최적화할 수 있는 Hook

위의 React.memo 대신 Hook 으로 사용할 수 있는 것 같다.

import React, { useState, useMemo } from "react";

const getAverage = (numbers) => {
  console.log("평균값 계산중..");
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState("");

  const onChange = (e) => {
    setNumber(e.target.value);
  };
  const onInsert = () => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber("");
  };

  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <div>
      <input value={number} onChange={onChange} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {avg}
      </div>
    </div>
  );
};

export default Average;

useMemo 끝에 있는 의존성 배열 [] 내에 적은 값이 업데이트 될 때만 useMemo내 첫번째 파라미터 실행


- useCallback : 렌더링 성능 최적화할 수 있는 Hook

useMemo와 비슷함. 이벤트 핸들러 함수를 필요할 때만 생성할 수 있다.

useCallback(() => {
    console.log('callback');
}, []);

useMemo(() => {
    const fn = () => {
        console.log('callback');
    };
    return fn;
}, []);

위의 두코드는 완전히 똑같은 코드이다.

useCallback은 useMemo로 함수를 반환하는 상황에서 더 편하게 쓸 수 있는 것이므로,

숫자, 문자열, 객체처럼 일반 값을 재사용하려면 useMemo를 사용하고

함수를 재사용하려면 useCallback을 사용하면 된다!


- useRef : 함수형 컴포넌트에서 ref를 쉽게 사용할 수 있는 Hook

import React, { useState, useMemo, useRef, useCallback } from 'react';

const getAverage = numbers => {
  console.log('평균값 계산중..');
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((a, b) => a + b);
  return sum / numbers.length;
};

const Average = () => {
  const [list, setList] = useState([]);
  const [number, setNumber] = useState('');
  const inputEl = useRef(null);

  const onChange = useCallback(e => {
    setNumber(e.target.value);
  }, []); // 컴포넌트가 처음 렌더링 될 때만 함수 생성
  const onInsert = useCallback(() => {
    const nextList = list.concat(parseInt(number));
    setList(nextList);
    setNumber('');
    inputEl.current.focus();
  }, [number, list]); // number 혹은 list 가 바뀌었을 때만 함수 생성

  const avg = useMemo(() => getAverage(list), [list]);

  return (
    <div>
      <input value={number} onChange={onChange} ref={inputEl} />
      <button onClick={onInsert}>등록</button>
      <ul>
        {list.map((value, index) => (
          <li key={index}>{value}</li>
        ))}
      </ul>
      <div>
        <b>평균값:</b> {avg}
      </div>
    </div>
  );
};

export default Average;

input에 ref 를 state값으로 주고, onInsert함수에서 state로 엘리먼트에 접근할 수 있다.

 

import React, { useRef } from "react";

const RefSample = () => {
    const id = useRef(1);
    const setId = (n) => {
        id.current = n;
    }
    const printId = () => {
        console.log(id.current);
    }
    return (
        <div>
            refsample
        </div>
    );
};

export default RefSample;

위처럼 작성하면 ref값 바뀌어도 컴포넌트 렌더링 안 됨!!

렌더링과 관련되지 않은 값을 관리할 때 useRef 쓰면 된다.