[TIL] 내일배움캠프 React 과정 2022.12.09_Redux

Today I Learned

  • React 숙련 강의 수강
  • CS DB 기초 특강 시청
  • Redux 특강 시청

 


React 숙련 강의

useState 복습하기

const [state, setState] = useState(initialState);

useState는 useState라는 함수가 배열을 반환하고, 이것을 구조 분해 문법으로 꺼내놓은 모습으로 이루어져 있다. 만약 state가 원시 데이터 타입이 아닌 객체 데이터 타입인 경우에는 불변성을 유지해줘야 한다.

 

 

함수형 업데이트

// 기존에 사용하던 방식
setState(number + 1);

// 함수형 업데이트 
setState(() => {});

// 현재 number의 값을 가져와서 그 값에 +1을 더하여 반환한 것.
setState((currentNumber)=>{ return currentNumber + 1 });

위 코드와 같이 setState의 ( ) 안에 수정할 값이 아니라, 함수를 넣을 수 있다. 그리고 그 함수의 인자에서는 현재의 state을 가져올 수 있고, { } 안에서는 이 값을 변경하는 코드를 작성할 수 있다. 

 

일반 사용법과 함수형 업데이트 방식의 차이점

// 일반 사용법
<button
    onClick={() => {
      setNumber(number + 1); 
      setNumber(number + 1); 
      setNumber(number + 1); 
    }}
>

// 함수형 업데이트
<button
    onClick={() => {
      setNumber((previousState) => previousState + 1);
      setNumber((previousState) => previousState + 1);
      setNumber((previousState) => previousState + 1);
    }}
>

일반 업데이트 방식 -> number가 1씩 증가

일반 업데이트 방식은 버튼을 클릭했을 때 setNumber가 각각 실행되는 것이 아니라, 배치(batch)로 처리한다. 즉 onClick을 했을 때 setNumber 라는 명령을 세 번 내리지만, 리액트는 그 명령을 하나로 모아 최종적으로 한 번만 실행을 시킵니다. 그래서 setNumber을 3번 명령하든, 100번 명령하든 한 번만 실행된다.

 

함수형 업데이트 방식 -> number가 3씩 증가

함수형 업데이트 방식은 3번을 동시에 명령을 내리면, 그 명령을 모아 순차적으로 각각 한 번씩 실행시킵니다. 0에 1더하고, 그다음 1에 1을 더하고, 2에 1을 더해서 3이라는 결과가 우리 눈에 보이는 것.

 

 

useEffect란?

useEffect는 리액트 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 설정할 수 있는 Hook이다. 어떤 컴포넌트가 화면에 보여졌을 때 무언가를 실행하고 싶거나 어떤 컴포넌트가 화면에서 사라졌을 때 무언가를 실행하고 싶다면 useEffect를 사용한다.

 

컴포넌트가 나타났을 때 (렌더링됐을 때 === 함수 컴포넌트가 실행됐을 때) useEffect의 effect 함수가 실행된다.

 

useEffect와 리렌더링(re-rendering)

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

const App = () => {
  const [value, setValue] = useState("");

  useEffect(() => {
    // 이 부분이 실행된다.
    console.log("hello useEffect");
  });

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(event) => {
          setValue(event.target.value);
        }}
      />
    </div>
  );
}

export default App;

 

useEffect는 useEffect가 속한 컴포넌트가 화면에 렌더링 될 때마다 실행된다. 이 때문에 의도치 않은 동작을 경험할 수도 있다.

  1. input에 값을 입력한다.
  2. value, 즉 state가 변경된다.
  3. state가 변경되었기 때문에 App 컴포넌트가 리렌더링 된다.
  4. 리렌더링이 되었기 때문에 useEffect가 다시 실행된다.
  5. 1번 → 5번 과정이 계속 순환한다.

 

의존성 배열(dependency array) 이란?

useEffect에는 의존성 배열이라는 것이 있는데  “이 배열에 값을 넣으면 그 값이 바뀔 때만 useEffect를 실행할게” 라는 것 것이다.

 

의존성 배열이 빈 배열인 경우

const App = () => {
  const [value, setValue] = useState("");
  useEffect(() => {
    console.log("hello useEffect");
  }, []); // 비어있는 의존성 배열

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(event) => {
          setValue(event.target.value);
        }}
      />
    </div>
  );
}

의존성 배열에 아무것도 넣지 않았으니 input에 어떤 값을 입력하더라도, 처음에 실행된 console.log("hello useEffect"); 이후로는 더 이상 실행이 되지 않는다. 이렇게 useEffect를 사용하는데 어떤 함수를 컴포넌트가 렌더링 될 때 단 한 번만 실행하고 싶으면 의존성 배열을 [ ] 빈 상태로 넣으면 된다.

 

의존성 배열에 값이 있는 경우

const App = () => {
  const [value, setValue] = useState("");
  useEffect(() => {
    console.log("hello useEffect");
  }, [value]); // value를 넣음

  return (
    <div>
      <input
        type="text"
        value={value}
        onChange={(event) => {
          setValue(event.target.value);
        }}
      />
    </div>
  );
}

value는 state이고 input을 입력할 때마다 그 값이 변하게 되니 useEffect도 계속해서 실행이 된다. -> console.log("hello useEffect");가 계속해서 찍힌다.

 

 

클린 업(clean up)이란?

컴포넌트가 사라졌을 때 무언가를 실행하는 과정을 클린 업 (clean up) 이라고 표현한다.

 

클린 업 기본 구조

const App = () => {

	useEffect(()=>{
	// 화면에 컴포넌트가 나타났을(mount) 때 실행하고자 하는 함수.

		return ()=>{
		// 화면에서 컴포넌트가 사라졌을(unmount) 때 실행하고자 하는 함수.
		}
	}, [])

	return <div>hello react!</div>
};

 

클린 업 예시

const 속세 = () => {
  const nav = useNavigate();

  useEffect(() => {
    return () => {
      console.log("안녕히 계세요 여러분! 전 이 세상의 모든 굴레와 속박을 벗어 던지고 제 행복을 찾아 떠납니다! 여러분도 행복하세요~~!");
    };
  }, []);

  return (
    <button onClick={() => {nav("/todos");}}>
      속세를 벗어나는 버튼
    </button>
  );
};

속세를 벗어나는 버튼을 누르면 useNavigate에 의해서 /todos로 이동하면서 속세 컴포넌트를 떠난다. 그러면서 화면에서 속세 컴포넌트가 사라지고, useEffect의 return 부분이 실행된다.

 

 

useEffect 정리

  • useEffect는 화면에 컴포넌트가 mount 또는 unmount 됐을 때 실행하고자 하는 함수를 제어하게 해주는 훅이다.
  • 의존성 배열을 통해 함수의 실행 조건을 제어할 수 있다.
  • useEffect 에서 함수를 1번만 실행시키고자 할 때는 의존성 배열을 빈 배열로 둔다.

 

React의 생명 주기(Life Cycle)와 mount 이해하기

리액트 컴포넌트에는 라이프사이클이 존재한다. 컴포넌트의 수명은 페이지에 렌더링 되기 전인 준비과정에서 시작하여 페이지에서 사라질 때 끝난다.

 

라이프사이클 메서드 종류는 총 9가지이다.

  • will 접두사가 붙은 메서드 👉 어떤 작업을 작동하기 전에 실행
  • Did 접두사가 붙은 메서드 👉 어떤 작업을 작동한 후에 실행

라이프사이클은 총 3가지로 Mount, Update, Unmount 카테고리로 나눈다.

 Mount 

DOM 객체를 생성하고, 이를 웹 브라우저에 출력하는 것. 즉, 기본 DOM에 컴포넌트를 렌더링 해 새로운 DOM 객체를 만들고, 이를 웹 브라우저에 출력한다.

  • constructor : 컴포넌트를 새로 만들 때마다 호출되는 클래스 생성사 메서드
  • getDerivedStateFromProps : props 에 있는 값을 state 에 넣을 때 사용하는 메서드
  • render : 준비한 UI를 렌더링하는 메서드
  • componentDidMount : 컴포넌트가 웹 브라우 저상에 나타난 후 호출하는 메서드

 

 Update 

props 및 state 값 변경, 부모 컴포넌트가 리렌더링 될 때, this.forceUpdate 실행을 통해 강제로 리렌더링하는 경우 업데이트된다.

  • getDerivedStateFromProps : 앞서 Mount 과정에서도 호출되고, props 변화에 따라 state 값에도 변화를 주고 싶을 때 사용
  • shouldComponentUpdate : 컴포넌트가 리렌더링을 해야 할지 말아야 할지를 결정, true 를 반환하면 다음 라이프사이클 메서드를 계속 실행, false 를 반환하면 작업을 중지(리렌더링 X)한다.
  • render : 컴포넌트를 리렌더링한다.
  • getSnapshotBeforeUpdate : 컴포넌트 변화를 DOM에 반영하기 바로 직전에 호출
  • componentDidUpdate : 컴포넌트의 업데이트 작업이 다 끝난 후 호출

 

 Unmount 

컴포넌트가 DOM에서 제거될 때 unmount가 진행된다.

  • componentWillUnmount : 컴포넌트가 웹 브라우저 상에서 사라지기 직전에 호출

 

출처 https://maivve.tistory.com/190

https://velog.io/@youngminss/React-컴포넌트-생명주기-메서드

 

React의 StrictMode란?

  • React.StrictMode는 애플리케이션의 잠재적인 문제를 알아내기 위한 도구이다.
  • StrictMode는 개발 모드에서만 활성화되기에, 빌드가 된 후의 프로젝트에서 StrictMode는 비활성화된다.
  • 개발을 진행하는 중 console.log()가 두 번 찍히는 이유는 StrictMode가 활성화되어있기 때문이다.

리액트는 렌더링 단계와 커밋 단계 두 가지의 단계로 동작한다. 렌더링 단계는 render 함수를 호출해서 이전 렌더와 비교를 수행하는 단계이고, 커밋 단계의 경우에는 라이프 사이클 함수를 실행시키며 DOM 노드를 추가/변경해주는 단계이다. 여기서 커밋 단계는 일반적으로 렌더링 단계보다 빠르다.

 

특정 메서드들은 여러 번 호출될 수 있기 때문에 원하는 결괏값을 보존하기 위해서 미리 잡아야 한다. 그럴 때 Strict 모드를 통해 2번 수행되는 메서드들을 잡아 미리 고칠 수가 있는 것이다. 물론 Strict 모드가 자동적으로 모든 부작용을 찾아낼 수는 없지만, 문제가 될 만한 함수를 두 번 실행하는 방법으로써 이러한 발견을 도와준다. 즉 Double-Invoke 방식을 통해 이를 우리에게 알려주는 것이다.

 

출처

https://velog.io/@citron03/React의-StrictMode에-대해서

https://velog.io/@kysung95/짤막글-react-strict-모드란

 

 

리덕스(Redux)란?

  • 리덕스는 전역 상태 관리 라이브러리이다.
  • 리덕스는 useState를 통해 상태를 관리했을 때 발생하는 불편함을 일부 해소시켜준다.
  • 리덕스는 중앙 State 관리소를 가지고 있으며, 모든 State는 이곳에서 생성된다.
  • useState로 생성한 State는 Local State이고, 리덕스에서 생성한 State는 Global State이다.

 

리덕스가 필요한 이유

useState의 불편함

  • 컴포넌트에서 컴포넌트로 State를 보내기 위해서는 반드시 부-모 관계가 되어야 한다.
  • 조부모 컴포넌트에서 손자 컴포넌트로 값을 보내고자 할 때도 반드시 부모 컴포넌트를 거쳐야만 한다. 즉, 정작 부모 컴포넌트에서는 그 값이 필요가 없어도 단순히 손자 컴포넌트에게 전달하기 위해 불필요하게 거쳐야만 하는 것을 의미한다. (조부모 → 부모 → 손자)
  • 자식 컴포넌트에서 부모 컴포넌트로 값을 보낼 수 없다.

 

Global state와 Local state

  • Local state (지역 상태) 란?
    • 컴포넌트에서 useState를 이용해서 생성한 state이다. 좁은 범위 안에서 생성된 State 라고 생각하면 된다.
  • Global state (전역 상태)란?
    • Global state는 컴포넌트에서 생성되지 않고 중앙화된 특별한 곳에서 생성된다. 쉽게 얘기해서 “중앙 state 관리소” 라고 생각하면 된다.
    • 중앙 State관리소에서 State를 생성하면 컴포넌트가 어디에 위치하고 있든 상관없이 State를 불러와서 사용할 수 있다. 이렇게 특정 컴포넌트에 종속되어 있는 것이 아니라 “중앙 state 관리소”에서 생성된 State를 Global state라고 하고 이러한 값들을 관리하는 것을 전역 상태 관리라고 한다.

 

 

prop drilling이란?

출처&nbsp;https://shishirarora3.medium.com/best-method-for-props-drilling-in-react-485605f26b5c

Prop Drilling 은 props를 오로지 하위 컴포넌트로 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 React Component 트리의 한 부분에서 다른 부분으로 데이터를 전달하는 과정이다.

 

 

리덕스 폴더 구조 생성하기

  • config : 리덕스 설정과 관련된 파일들을 놓을 폴더. (configuration=환경 설정)
  • configStore.js : “중앙 state 관리소"인 Store를 만드는 설정 코드들이 있는 파일.
  • modules : 우리가 만들 State들의 그룹이다. 예를 들어 투두리스트를 만든다고 한다면, 투두리스트에 필요한 state들이 모두 모여있을 todos.js를 생성하게 되는데 todos.js 파일이 곧 하나의 모듈이 된다.

 

리덕스 설정 코드 이해하기

configStore.js

import { createStore } from "redux";
import { combineReducers } from "redux";

const rootReducer = combineReducers({}); 
const store = createStore(rootReducer); 

export default store;

1. createStore()

리덕스의 가장 핵심이 되는 스토어를 만드는 메소드(함수) 이다. 리덕스는 단일 스토어로 모든 상태 트리를 관리하기 때문에 creatorStore는 한 번만 호출하면 된다.

 

2. combineReducers()

리덕스는 action —> dispatch —> reducer 순으로 동작하는데 애플리케이션이 복잡해지게 되면 reducer 부분을 여러 개로 나눠야 하는 경우가 발생한다. combineReducers은 여러 개의 독립적인 reducer의 반환 값을 하나의 상태 객체로 만들어 준다.

 

index.js

// 원래부터 있던 코드
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

// 추가할 코드
import store from "./redux/config/configStore";
import { Provider } from "react-redux";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  // App을 Provider로 감싸고, configStore에서 export default한 store를 넣는다.
  <Provider store={store}> 
    <App />
  </Provider>
);

 

 

모듈 만들기

모듈이란? State들의 그룹

리듀서란? 변화를 일으키는 '함수'

// modules 폴더에 counter.js 파일을 생성한다.
// src/modules/counter.js

// 초기 상태값
const initialState = {
  number: 0,
};

// 리듀서
const counter = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

// 모듈파일에서는 리듀서를 export default 한다.
export default counter;

 

 

모듈의 구성요소 살펴보기

 initialState = 초기 상태값 

// 초기 상태값
const initialState = 0;

// 초기값이 number = 0, name = '석구'인 객체
const initialState = {
    number: 0,
    name: '석구'
};

State의 초기값을 정해주는 부분으로 useState를 사용했을 때 괄호 안에 초기값을 지정해주던 것과 같은 것이다. State의 초기값은 객체, 배열, 원시 데이터 모두 가능하다. 객체 안에 여러 개의 변수를 넣어줄 수도 있다.

 

 Reducer = 변화를 일으키는 함수 

// 리듀서 
const counter = (state = initialState, action) => {
  switch (action.type) {
    default:
      return state;
  }
};

export default counter;

리듀서의 인자의 첫 번째 자리에서는 state를, 두 번째 자리에서는 action을 꺼내서 사용할 수 있다. state = intialState 처럼 state에 initialState를 할당해줘야 하는 것을 기억하기!

 

 configStore.js 에서 카운터 모듈을 스토어에 연결하기

// src/redux/modules/config/configStore.js

// 원래 있던 코드
import { createStore } from "redux";
import { combineReducers } from "redux";

// 새롭게 추가한 부분
import counter from "../modules/counter";

const rootReducer = combineReducers({
  counter: counter, // <-- 새롭게 추가한 부분
});
const store = createStore(rootReducer);

export default store;

 

 

스토어와 모듈 연결 확인하기

 useSelector = 스토어 조회 

// 1. store에서 꺼낸 값을 할당 할 변수를 선언합니다.
const number = 

// 2. useSelector()를 변수에 할당해줍니다.
const number = useSelector() 

// 3. useSelector의 인자에 화살표 함수를 넣어줍니다.
const number = useSelector( ()=>{} )

// 4. 화살표 함수의 인자에서 값을 꺼내 return 합니다. 
// 우리가 useSelector를 처음 사용해보는 것이니, state가 어떤 것인지 콘솔로 확인해볼까요?
const number = useSelector((state) => {
	console.log(state)
	return state
});

컴포넌트에서 생성한 모듈을 스토어를 잘 연결했는지 조회할 때는 react-redux에서 제공하는 useSelector 라는 훅을 사용한다.

 

// src/App.js

import React from "react";
import { useSelector } from "react-redux"; // import 해주세요.

const App = () => {
  const counterStore = useSelector((state) => state); // 추가해주세요.
  console.log(counterStore); // 스토어를 조회해볼까요?

  return <div></div>;
}

export default App;

화살표 함수에서 꺼낸 state라는 인자는 현재 프로젝트에 존재하는 모든 리덕스 모듈의 state이다. counterStore는 콘솔 창에 찍으면 객체 형태로 나타난다. useSelctor로 확인할 때 변수명은 아무렇게나 해도 상관없는 거 같다.

 

const number = useSelector(state => state.counter.number); // 0

만약 컴포넌트에서 number라는 값을 사용하고자 한다면 상단의 코드처럼 꺼내서 사용하면 된다.

 

// 원래 구조
const counterStore = useSelector(function(state){
 return state;
})

// 화살표 함수로 간단하게 작성
const counterStore = useSelector((state) => state);

화살표 함수로 코드 작성하기

 


React 입문 과제 수정

form 태그 추가하고 주소창에 내용이 뜨는 문제

input 태그에 method="post"를 추가하면 주소창에 입력한 내용이 보이지 않는 거 같다.

참고 https://velog.io/@florence_y/TIL-Form

 

 

컴포넌트 구조 수정

원래는 Header, Form, TodoCard(->Button)으로 컴포넌트가 구성되어 있었는데 이렇게 하니까 이미지처럼 working과 done에 따라서 비슷한 구조가 두 번 반복되어서 불필요한 코드가 작성되었다. 그래서 컴포넌트 구조를 Header, Form, TodoList(->TodoCard->Button)으로 수정했다.

 

그랬더니 최상위 App 컴포넌트가 이렇게 간단한 구조가 되었다. 확실히 한눈에 구조를 파악하기 쉬운 거 같다.

 

 

컴포넌트 하나로 working과 done 부분을 각각 가져오는 방법

function TodoList (props) {
    const { todo, isDone, changeDoneHandler, deleteHandler } = props;
        return (
            <div className="list">
                <h2>{ isDone ? "Done..! 🎉" : "Working.. 🔥"}</h2>
                <div className="list-container">
                    {todo.filter((list) => list.isDone === isDone)
                    .map((list) => {
                        return (<TodoCard changeDoneHandler={changeDoneHandler} deleteHandler={deleteHandler} list={list} key={list.id} />
                    );
                    })}
                </div>
            </div>
        );
};

원래는 app 컴포넌트에서 map을 두 번 돌려서 각각 if문에 isDone이 false일 때, true일 때의 리스트를 가져오는 구조였는데 컴포넌트를 따로 빼면서 어떻게 하나의 컴포넌트에서 두 개의 리스트를 가져와야 되는지 모르겠다. 그러니까 지금 작성한 코드로는 isDone이 false인 경우만 불러오고 있고 true인 부분은 가져오지 못했다. map을 돌리는 부분은 list-container div인데 어떻게 list div까지 가져와서 두 개를 만들 수 있는지가 궁금하다.

 

const App = () => {
    // 생략...
    return (
        <div className="wrap">
          <Header />
          <Form 
            title={title} 
            content={content} 
            inputContent={inputContent} 
            addHandler={addHandler} 
          />
          <TodoList 
            todo={todo} 
            isDone={false}
            changeDoneHandler={changeDoneHandler}
            deleteHandler={deleteHandler}
          />
          <TodoList 
            todo={todo} 
            isDone={true}
            changeDoneHandler={changeDoneHandler}
            deleteHandler={deleteHandler}
          />
        </div>
  );
}

드디어 문제점을 찾았다!! 그동안 계속 TodoList 컴포넌트만 보고 있었는데 문제는 App 컴포넌트에 있었다. App 컴포넌트에 TodoList 컴포넌트를 하나만 넣어 놨었는데 TodoList를 두 개 넣어주면 되는 거였다! 근데 하나는 isDone에 false 값을, 하나는 true 값을 넣어주면 된다. 와 드디어 해결~~ 어쩐지 TodoList에서 filter랑 map이 안쪽에 있어서 이상하다 싶었더니 그냥 간단하게 TodoList 컴포넌트를 두 개 넣으면 되는 것을 ㅎㅎ

 

 

"JSX 요소 'div'에 닫는 태그가 없습니다" 에러

혹시나 해서 vscode 창을 다시 켰다가 에러가 사라졌다..!

 

 

React에서 폰트 추가하기

@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR&display=swap');

.wrap{
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 50px;
  font-family: 'Noto Sans KR', sans-serif;
}

CSS 파일에 바로 import하고 폰트 넣어주면 된다. 너무 쉽다~~


 

Programmers 문제 풀기

영어가 싫어요

시도 방법 1

원래 원했던 건 배열 안에 [1, 2, 3...] 이런 식으로 숫자를 추가해서 join()을 이용해서 배열을 문자열로 전환하는 것이었는데 실패했다. array에 push를 하니까 문자열 전체가 중복해서 배열에 추가된다.


 

회고

구글링 하다가 벨로그에서 재밌는 코드를 발견했다 ㅋㅋ

const isInChallenge = true; 
const hasStrongWill = true;  
(() => {while (isInChallenge) {  
        	if(hasStrongWill) {
            	 	return 'Success' 
        	} 
        }
)();

출처 https://velog.io/@kich555

 

오늘은 리액트 입문 과제 투두리스트 코드 수정하고 리액트 숙련 강의도 들었다. 리덕스를 처음으로 공부했는데 props 넘기면서 불편했던 부분을 리덕스에서 해결할 수 있다고 해서 기대가 된다. 생각해 보니까 리액트 공부를 시작한 지 5일밖에 안 됐는데 벌써 어느 정도 익숙해진 거 같다. 처음에는 개념도 이해가 잘 안 되고 내가 할 수 있을까 걱정됐었는데 과제를 수행하면서 컴포넌트의 구조라든지 state, props 등에 대해서 많이 배웠다.

 

저녁에는 팀원 한 분의 문제를 (페이지 연결하기) 놓고 팀원들과 같이 얘기하며 해결했다. 오늘 느낀 게 다른 사람 코드 보면서 문제점 찾기가 쉽지 않아서 튜터님들이 대단하신 거 같다는 생각도 들었다 ㅋㅋ 주말 동안 리액트 숙련 강의 다 듣는 것을 목표로 하고 오늘 하루도 마무리한다!