마이페이지에서 화면에 보여줘야할 정보가 프로필 이미지와 닉네임이었기때문에 프로필 이미지와 닉네임은 useState 를 사용해줬다. 그리고 이번 과제를 하면서 supabase를 처음 다루어보았는데 우리가 Auth 테이블과 member 테이블 두개를 만들어놓은 상태로 개발을 진행한 상태라서 프로필을 변경할 땐 두 테이블 다 데이터를 업데이트해줘야했다.
추가하는 리듀서함수를 만들어보자. 리듀서 함수는 두가지 매개변수를 갖는다. state(기존 객체) 와 action(업데이트할 객체) . 이 두가지 매개변수를 가지고 상태를 업데이트하고 최신 상태를 store에 제공해준다.
내 코드는 우선 월별 지출내역이 없다면 빈배열로 반환하도록 되어있고, 새로운 지출내역을 월별 지출내역에 push 해준다. 그리고 로컬스토리지에 저장. 여기서 내가 받아야할 action.apyload는 선택한 월과 새로운 지출내역이다. 프롭스 드릴링이나 context 를 사용했다면 추가 후에 setState를 사용해 상태를 업데이트해줘야하는데 리듀서 함수 자체가 setState 역할을 하기 때문에 할 필요가 없다.
useSelector 로 구독할 대상을 설정한다. 구독해야할 대상은 지출내역 데이터가 담긴 expense slice와 어떤 월을 선택했는지 확인할 수 있는 month slice 두개이다. 그리고 useDispatch로 리듀서 함수를 호출해준다. 대신 액션객체를 담아서! 액션 타입에 따라 상태를 어떻게 업데이트해주는지 리듀서 함수에서 정의해주기 때문이다.
위 파일에서 삭제, 수정 리듀서 함수를 생성해준다. 삭제는 id 를 액션 페이로드로 받고, 수정은 수정된 데이터를 액션 페이로드로 받아줬다. 초기값이 객체형태였기 때문에 filter(),map() 메서드를 사용하지 못해서 for-in 문으로 객체 month를 키값으로 순회해서 배열로 가지고 왔다. 각 월 지출 내역 데이터를 expensesOfMonth 변수에 할당해주고 filter를 사용해 삭제할 데이터를 제외한 나머지 데이터들을 반환하도록 해주고 반환된 데이터를 기존 월별 데이터에 재할당해주었다. 그리고 로컬스토리지에 저장.
useSelector로 월 지출내역을 가지고와서 상세페이지에 노출시키려고 보니 오류가 발생했다. 확인해보니 내가 가지고온 데이터는 전체 월 지출내역. 상세페이지에서 필요한 데이터는 해당 월의 지출내역. 완전히 다른 데이터를 가지고 온 것이다. 그래서 가지고온 데이터를 내가 선택한 월과 같은 데이터만 가지고 오도록 find 메서드를 사용해주었다.
여기서 살짝 헤맸는데 해당 지출 내역을 가져와서 map메서드로 각 지출 내역을 하나씩 만들어준다. 그래서 ExesItem.jsx에도 useContext를 사용해서 데이터를 가져와야하나? 했는데 사용하기 어려워보였다. 튜터님께 여쭤보니 해당 과제가 context를 사용해보려고 하는것이지 아예 props-drilling을 사용하지 말라는건 아니라고 하셨고, 데이터를 바로 전달할 수 있는 경우에는 props로 하위 컴포넌트에게 넘겨주어도된다고 했다.
import React, { useContext } from "react";
import ExesItem from "./ExesItem";
import { Context } from "../context/Context";
const ExesList = () => {
const { filteredList } = useContext(Context);
return (
<div>
<ul>
{/*
Uncaught TypeError: Cannot read properties of undefined (reading 'map')
커밋 된 후에야 모든 효과를 실행
React는 return에서 map을 반복실행할 때 첫 턴에 데이터가 아직 안들어와도 렌더링이 실행되며
당연히 그 데이터는 undefined로 정의되어 오류
&& 사용
true && expression은 항상 expression으로 실행
*/}
{filteredList &&
filteredList.map((exe) => {
return <ExesItem key={exe.id} exe={exe} />;
})}
</ul>
</div>
);
};
export default ExesList;
삭제할 지출 내역을 제외한 새로운 배열을 생성하고, 그 배열을 포함한 새로운 객체를 만든다. 불변성 유지를 위해 전체 월 지출내역 복사, removeExes 안에는 전체 지출 내역 중 삭제된 지출내역을 제외한 나머지 지출내역이 들어가 있다. 로컬스토리지에 해당 데이터를 저장하고 setExes(지출 내역 업데이트)로 상태 업데이트를 해준다.
리덕스를 사용하면 상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 된다.
👾 useState의 불편함
- 컴포넌트에서 컴포넌트로 State를 보내기위해서는 반드시 부-모 관계여야한다.
- State를 전달해주기만 하는 List 컴포넌트에도 불필요하게 State 값을 내려주어야한다.
- 자식 컴포넌트에서 부모 컴포넌트로 값을 보낼 수 없다.
- 자식이 많아진다면 상태 관리가 매우 복잡해진다
- 상태를 관리하는 상위 컴포넌트에서 계속 내려 받아야한다. (Props drilling 이슈)
✨ 리덕스를 통해 해결가능한 점
1. 전역 상태 저장소 제공
2. Props drilling 이슈 해결
✏️ 내가 이해한대로 그리는 리덕스 구조
간단한 로직이다. view에서 상태 변화가 생겼다면 dispatch 메서드에 액션 객체(데이터)를 담아 reducer를 호출해준다. dispatch에 전달인자로 담긴 action을 reducer에서는 매개변수로 받아 실행한다. 그리고 액션의 타입에 따라 상태를 업데이트 해주고 store에 변경된 최신 상태를 제공한다. 마지막으로 useSelector 를 통해 store에서 초기/최신 상태를 받아 UI 를 렌더링한다.
📌 Action : { type, payload }
상태 변화를 일으키는 객체이다. type은 필수 속성이고 payload는 선택 속성이다. 추가적인 데이터를 받을 때 사용한다.
📌Dispatch
액션 객체를 전달인자로 받아 리듀서를 호출하는 함수이다. 리듀서 함수는 dispatch를 통해 액션 객체를 매개변수로 받는다.
📌Reducer
기존 상태와 액션 객체를 매개변수로 가지며 이 두가지를 참조하여 상태를 변경한다. 액션 객체에서 action.type에 따라 상태를 어떻게 변경할지 정의하는 함수이다. 상태 변경 후 최신 상태를 Store에게 제공한다.
1. 스토어 생성 및 스토어 구독
B 컴포넌트에서 일어난 상태 변화가 G 컴포넌트에 반영된다고 가정해보자.
스토어를 생성한 후에 G 컴포넌트에서 useSelector 로 store을 구독하고 state의 최신상태를 받는다.
useSelector((state) => state.구독대상)
2. 상태 변경 요청
B 컴포넌트에서 상태를 업데이트할 일이 생긴다. 이 때 dispatch라는 함수에 액션을 담아 store에 상태 업데이트를 요청한다.
dispatch 함수에 담은 액션을 전달인자라고 하는데 이는 리듀서를 호출할 때 필요한 매개변수로 사용한다.
3. 리듀서로 상태 업데이트
B 컴포넌트로부터 전달받은 action 객체의 타입에 따라 상태를 업테이트 해줘야한다. 이때 액션의 타입에 따라 어떻게 상태를 업데이트 해주는지 정의하는 함수를 리듀서 함수라고 한다. 아래 두가지 매개변수를 참조하여, 새로운 상태 객체를 만들어서 최신 상태를 반환한다.
📌 reducer 함수는 두가지 매개변수를 갖는다.
- state : 현재 상태
- action : 액션 객체
4. 구독 알림
위 과정을 다 거치면 컴포넌트는 새로운 상태를 받게되고, 이에 따라 컴포넌트는 리렌더링한다.
Redux 설치
yarn add redux react-redux
파일 생성
📂 redux : 리덕스와 관련된 코드를 모두 모아 놓을 폴더
📂 config : 리덕스 설정과 관련된 파일들을 놓을 폴더
📂 cofigStore.js : 중앙 state 관리소
📂 module : State들의 그룹 폴더
📂 text.js :하나의 모듈
Store 생성
combineReducers은 여러 개의 독립적인 reducer의 반환 값을 하나의 상태 객체로 만든다.
import { combineReducers, createStore } from "@reduxjs/toolkit";
import texts from "../module/text";
// 1. combineReducers로 리듀서 조합
const rootReducer = combineReducers({ texts });
// 2. 조합한 리듀서들로 스토어 생성
const store = createStore(rootReducer);
export default store;
리듀서 생성
texts라는 리듀서에 onAddText라는 액션 타입을 만들어준다. 이때 texts(리듀서)는 state와 action 두개의 매개변수를 갖는다. 두개의 매개변수를 참조하여 상태를 업데이트한다. 아래 코드를 보면 초기값은 로컬스토리지에 저장되어있는 데이터가 있다면 불러온 데이터가 초기값이 되고 없다면 빈 배열이 초기값이 된다.
액션 타입 onAddText인 경우 처리는 다음과 같다. 기존 상태(...state)를 복사하여 변경하고, text 속성에 새로운 배열을 할당한다. 기존 text 배열과 새로운 항목(action.payload)을 합쳐서 새로운 배열을 생성한다.
화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx
할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx
todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx
진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx
현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx
배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.
App.jsx
Todo의isDone 상태가 true이면, 상태 버튼의 라벨을취소, isDone이 false 이면 라벨을완료로 조건부 렌더링이 필요해서 토글 함수를 만들었다. 삭제 함수와 마찬가지로 id 값을 받아와서 처리하도록 했다.
map과 삼항연산자를 활용했다. map()으로 배열 순회를 돌면서 배열의 id와 사용자가 선택한 id 가 같다면, todo에 isDone 키의 값이 true -> false, false -> true 로 반전시켜주는 NOT 연산자를 사용해줬다. 같지 않다면 그대로 todo가 나오도록 해주었다.
TodoList 에서 사용할 수 있게 props로 내려준다. 그리고 진행중인 리스트와 진행 완료된 리스트를 나누어서 완료된 리스트는 진행 중인 리스트 아래로 내려야 하기때문에 TodoList 를 두개 만들어주고 안에 들어가는 props 에 filter를 써서 조건부 렌더링이 되도록 해주었다.
화면을 가운데에 정렬시켜주며, 일정 리스트를 보여줍니다. children으로 내부 JSX를 props로 받아 와서 렌더링해줍니다.
TodoAdd.jsx
할 일을 추가하는 컴포넌트 입니다.
TodoList.jsx
todos 배열을 props로 받아 온 후, 이를 배열 내장 함수 map을 사용해서 여러 개의 TodoItem 컴포넌트로 변환하여 보여 줍니다.
TodoItem.jsx
진행중인 할 일 정보를 보여주는 컴포넌트입니다. todo 객체를 props로 받아 와서 상태에 따라 다른 스타일의 UI를 보여줍니다
CurrentDate.jsx
현재 날짜를 알려주는 컴포넌트입니다.
Icons.jsx
배경에 꾸며주는 Sticker 요소를 보여주는 컴포넌트입니다.
🐣 프로젝트 생성
1. CRA(Create React App)
아래처럼 성공 메세지가 띄워졌다면 app 생성 성공
yarn create react-app [원하는 프로젝트 이름]
생성한 App 파일로 Change Directory 하고 yarn을 실행해준다. 그럼 리액트 앱 생성 완료~~ EZEZ~~
cd [생성한 App 이름]
yarn start
2. Vite
yarn create vite [생성할 App 이름] --template react
생성 완료닷
cd [생성한 App 이름]
yarn
yarn dev
Vite로 생성하면 좋은 이유
WebPack을 사용하는 CRA 대신 Esbuild를 사용하는 Vite는 원래 Vue.js 애플리케이션을 위해 만들어졌지만,
현재는 React, Svelte, Vanilla JS 등 다양한 프레임워크와 라이브러리를 지원한다.
1. 빠른 콜드 스타트와 HMR(Hot Module Replacement)
2. 속도 측면에서 기존 CRA와는 비교가 되지 않을 정도로 빠르다
3. CRA는 기본적으로 설정을 숨기지만, Vite는 사용자가 필요에 따라 설정을 더 쉽게 조정할 수 있다
4. Go 언어 베이스의 자바스크립트 빌드 툴입니다. CRA가 채택하는 웹팩과 비교할 때, 말이 안되는 수준의 속도
🐥 UI 구현하기
우선 App.jsx에 리스트 배열을 만들어주고 각 컴포넌트를 배치한다. TodoList가 두개인 것은 진행 중인 리스트 목록과 완료한 리스트 목록을 UI로 구분하기 위함이다.
import React, { useState } from "react";
import "./App.css";
import TodoTemplate from "./component/TodoTemplate";
import TodoAdd from "./component/TodoAdd";
import CurrentDate from "./component/CurrentDate";
import TodoList from "./component/TodoList";
컴포넌트를 앞으로도 잘 이용할것이라면 import하는 것을 잊지말자. 하면서도 import 오류가 발생하면 아차 싶은 1인..
TodoTemplate.jsx
템플릿은 이렇게 구현해주었다. children 으로 내부 JSX를 가져와서 템플릿 안 어떤 컨텐츠든 자유자재로 바뀌도록 해준다.
contents로 받은 값으로 새로운 객체를 생성해 기존 객체에 추가해준다. 여기서 스프레드 연산자를 사용한 이유는 불변성을 유지하기 위해서이다. 배열이니 push로 추가해도되지 않을까 할 수 있지만 push한다면 배열에 직접적으로 추가하는 것이기 때문에 값은 변할지 몰라도 메모리 주소는 바뀌지 않는다. 배열 중간에 것을 삭제하면 오류가 날 수 있고, 이것은 불변성 유지가 불가능하다고 볼 수 있다.
그래서 스프레드 연산자로 기존 객체를 복사해주고 그 뒤로 생성한 객체를 배열에 넣어주었다. 이러면 불변성을 유지할 수 있다.
// 할 일 추가
const onInsert = (contents) => {
const newTodo = {
id: Date.now(),
contents: contents,
};
setTodo([...todos, newTodo]);
};
onInsert 함수를 만들어주었으니 추가 컴포넌트에서 사용할 수 있도록 props로 받아오자.
<TodoAdd onInsert={onInsert} />
추가 컴포넌트에서 기능 구현을 위해 사용한 리액트 Hook은 useState(), useRef(), useEffect()이다. 차례대로 이해한대로 정리를 해보면 아래와 같다.
✅ useSatae() : 컴포넌트의 상태를 관리해준다. 가변하는 사용자 입력 값을 동적으로 사용하기 위함.
✅ useRef() : current라는 키값을 지닌 프로퍼티가 생성되고, 값에 어떤 변경을 줄때도 current를 이용한다. DOM요소에 접근이 가능하면, 불필요한 재렌더링을 하지 않는다는 장점이 있다.
✅ useEffect() : 컴포넌트가 렌더링될때마다 특정 작업을 실행 할 수 있다. (처음 시작할때 / 사라질때 / 업데이트 될때
우선, useState로 상태를 선언해준다. 초기값은 빈 문자열로 해준다. 위에 새로운 객체를 추가하는 함수 onInsert를 만들때 contents 값을 받아와 객체를 생성해주도록 했기때문에 onInsert(contents=사용자로부터 받아온 값) 으로 넣어준다. 생성할때 onClick이 아니라 onSubmit를 사용해준 이유는 form 태그를 사용했기 때문이다. 🍯 onSubmit 은 form 태그 내부에 이벤트를 실행할 수 있다. form 제출 이벤트가 발생할 때의 동작을 지정한다.
이때 오류가 났었는데 console.log로 확인했을 땐 값이 잘 들어가는데 새로고침되면서 값이 안보이는 오류가 생겼다. 이럴때에는 e.preventDefault(); 를 사용해주면 된다.preventDefault 를 통해 이러한 동작을 막을 수 있다.
e.preventDefault();
추가로 나는 + 버튼을 눌렀을 때 input창을 띄우는 작업을 하고 싶었다. 감이 안와서 튜터님께 여쭤보니 하고싶은 기능을 말로 해보라고 하셨다. "+ 버튼을 눌렀을 때, 인풋창을 띄우고 싶어요." 말 그대로다 처음에 useState로 인풋창의 상태를 false로 지정해주었다.
그리고 버튼을 눌렀을 때를 코드로 작성해보면 아래와 같다. 난 왜 응용을 못할까.. 🤬 말 그대로 버튼을 눌렀을 때 부정 연산자를 사용해서 상태를 반전(true - false, false - true) 해주는 것이다. 버튼에서 open 상태를 사용할 수 있게 속성으로 가지고 왔다.
이제 +를 누르면 인풋창이 뜨겠지? 했지만 오류.. 왜그런가 했더니 open 상태가 true가 되었을 때 어떤 화면이 렌더링 되는지 안정해줬기 때문이다. inserPosition 이 클래스를 띄우면 되는데 어찌할까 구글링하다 논리곱연산자로 구현할 수 있는 방법을 찾았다.
논리곱연산자는(&&) 좌변 true일 때 우변을 실행한다. 아래 코드를 해석해보자면 속성에 open이 true 이면 <div> 태그를 실행해줘~ 다.