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가 속한 컴포넌트가 화면에 렌더링 될 때마다 실행된다. 이 때문에 의도치 않은 동작을 경험할 수도 있다.
- input에 값을 입력한다.
- value, 즉 state가 변경된다.
- state가 변경되었기 때문에 App 컴포넌트가 리렌더링 된다.
- 리렌더링이 되었기 때문에 useEffect가 다시 실행된다.
- 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이란?
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'
}
}
)();
오늘은 리액트 입문 과제 투두리스트 코드 수정하고 리액트 숙련 강의도 들었다. 리덕스를 처음으로 공부했는데 props 넘기면서 불편했던 부분을 리덕스에서 해결할 수 있다고 해서 기대가 된다. 생각해 보니까 리액트 공부를 시작한 지 5일밖에 안 됐는데 벌써 어느 정도 익숙해진 거 같다. 처음에는 개념도 이해가 잘 안 되고 내가 할 수 있을까 걱정됐었는데 과제를 수행하면서 컴포넌트의 구조라든지 state, props 등에 대해서 많이 배웠다.
저녁에는 팀원 한 분의 문제를 (페이지 연결하기) 놓고 팀원들과 같이 얘기하며 해결했다. 오늘 느낀 게 다른 사람 코드 보면서 문제점 찾기가 쉽지 않아서 튜터님들이 대단하신 거 같다는 생각도 들었다 ㅋㅋ 주말 동안 리액트 숙련 강의 다 듣는 것을 목표로 하고 오늘 하루도 마무리한다!