Thunder Client ❓

 

Thunder Client 는 VSCode에서 사용할 수 있는 HTTP 클라이언트 확장 프로그램 중 하나로 VSCode 내에서 HTTP 요청을 생성하고 테스트할 수 있다.

 

 

 

⚡️확장 프로그램 설치

 

 

⚡️확장 프로그램 사용방법

New Request

설치를 완료하고 나면 왼쪽 툴에 번개 모양 아이콘이 생긴다. 방금 설치한 Thunder Client다. 번개 아이콘을 누른 후 New Request 버튼을 차례대로 누른다.

 

 

간단한 HTTP 요청

GET 외부로부터 데이터 가져오기
POST 데이터 생성
PUT 데이터 업데이트
PATCH 데이터 일부 업데이트
DELETE 데이터 삭제

 

이제 마지막 ! context 에 이어 리덕스 툴킷 라이브러리로 전역상태 관리하기!

 

 

📌 Context로 만든 지출 관리 사이트

https://ejunyang.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B0%9C%EC%9D%B8-%EC%A7%80%EC%B6%9C-%EA%B4%80%EB%A6%AC-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%B9%EB%93%A4%EA%B8%B0-Context%F0%9F%90%A2

 

[리액트] 개인 지출 관리 사이트 맹들기 - Context🐢

props-drilling에 이어 context로 props를 전달해서 기존 기능들이 정상적으로 작동되게 만들어보자!!🔥   📌 props-drilling으로 만든 지출 관리 사이트https://ejunyang.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA

ejunyang.tistory.com

 

 

 

컴포넌트

컴포넌트 설명
ExesForm.jsx 지출내역을 입력하는 컴포넌트이다. 메인 페이지 우측 하단 버튼을 누르면 모달처럼 뜨도록 구현했다.
MonthList.jsx 월별 버튼 컴포넌트이다. 해당 월을 누를 때마다 다른 스타일링이 들어간다.
ExesList.jsx 월별 버튼을 눌렀을 때 하단에 해당 월의 지출내역이 나타나는 지출 내역 리스트 컴포넌트이다.
ExesItem.jsx 각 지출 내역의 컴포넌트이다.

 

 


 

 

리덕스 설치 및 생성

📂 redux >  📄 configStore.js

yarn add @reduxjs/toolkit react-redux

 

우선 만들어야할 리듀서 함수는 지출 내역과 월버튼이다. 매번 월별 지출내역을 초기값으로 가져와야하기때문에 전역에서 상태관리하는게 좋겠다고 생각이 들었다. 

import { configureStore } from "@reduxjs/toolkit";
import expense from "../slices/expense";
import month from "../slices/month";

const store = configureStore({
  reducer: {
    expense: expense,
    month: month,
  },
});

export default store;

 

 

 

📂 redux >  📂 slices > 📄 expense.js

 

지출 내역 초기값을 세팅해주고, 추가, 수정, 삭제 리듀서 함수를 만들어보자

.

inisialState는 초기값을 나타낸다. 로컬스토리지에 expenseList 변수명으로 저장되어있는 데이터를 객체화하여 가져온다. 데이터가 있으면 객체화해서 가지고오고, 없다면 빈 객체로 반환한다. 

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
  expenseList: localStorage.getItem("expenseList")
    ? JSON.parse(localStorage.getItem("expenseList"))
    : {},
};

const expense = createSlice({
  name: "expense",
  initialState: initialState,
  reducers: {},
});

export const { addExpense } = expense.actions;
export default expense.reducer;

 

 

추가하는 리듀서함수를 만들어보자. 리듀서 함수는 두가지 매개변수를 갖는다. state(기존 객체) 와 action(업데이트할 객체) . 이 두가지 매개변수를 가지고 상태를 업데이트하고 최신 상태를 store에 제공해준다.

내 코드는 우선 월별 지출내역이 없다면 빈배열로 반환하도록 되어있고, 새로운 지출내역을 월별 지출내역에 push 해준다. 그리고 로컬스토리지에 저장. 여기서 내가 받아야할 action.apyload는 선택한 월과 새로운 지출내역이다. 프롭스 드릴링이나 context 를 사용했다면 추가 후에 setState를 사용해 상태를 업데이트해줘야하는데 리듀서 함수 자체가 setState 역할을 하기 때문에 할 필요가 없다.

addExpense: (state, action) => {
      const { selectedMonth, newExes } = action.payload;
      if (!state.expenseList[selectedMonth]) {
        state.expenseList[selectedMonth] = [];
      }
      state.expenseList[selectedMonth].push(newExes);
      localStorage.setItem("expenseList", JSON.stringify(state.expenseList));
      return state;
    },

 

 

📂 redux >  📂 slices > 📄 month.js

선택한 월의 지출내역을 볼 수 있도록 month slice도 생성해주었다. 

import { createSlice } from "@reduxjs/toolkit";

let selectedMonth = JSON.parse(localStorage.getItem("selectedMonth"));

if (!selectedMonth) {
  selectedMonth = new Date().getMonth();
}

const initialState = {
  selectedMonth: selectedMonth,
};

const monthSlice = createSlice({
  name: "month",
  initialState: initialState,
  reducers: {
    setSelectedMonth: (state, action) => {
      state.selectedMonth = action.payload;
    },
  },
});

export const { setSelectedMonth } = monthSlice.actions;
export default monthSlice.reducer;

 

리듀서 함수 생성 후 다른 컴포넌트에서 사용할 수 있게 export 해줘야한다는 것을 잊지 말자.

export const { addExpense } = expense.actions;
export default expense.reducer;
export const { setSelectedMonth } = monthSlice.actions;
export default monthSlice.reducer;

 

 

 

 

 

 

Home.jsx

이제 추가하는 함수가 필요한 컴포넌트에 적용해보도록하자!

useSelector 로 구독할 대상을 설정한다. 구독해야할 대상은 지출내역 데이터가 담긴 expense slice와 어떤 월을 선택했는지 확인할 수 있는 month slice 두개이다. 그리고 useDispatch로 리듀서 함수를 호출해준다. 대신 액션객체를 담아서! 액션 타입에 따라 상태를 어떻게 업데이트해주는지 리듀서 함수에서 정의해주기 때문이다.

import { useDispatch, useSelector } from "react-redux";
import { addExpense } from "../redux/slices/expense";

const Home = () => {
  const dispatch = useDispatch();
  const exes = useSelector((state) => state.expense.expenseList);
  const selectedMonth = useSelector((state) => state.month.selectedMonth);

  console.log("selectedMonth1", selectedMonth);
  const handleMonthSelect = (idx) => {
    dispatch(setSelectedMonth(idx));
  };

  const onInsert = useCallback((date, item, amount, desc) => {
    dispatch(
      addExpense({
        selectedMonth: selectedMonth,
        newExes: {
          id: uuidv4(),
          date,
          item,
          amount,
          desc,
          month: selectedMonth,
        },
      })
    );
  });

 

 

새로운 지출내역을 담아주는 onInsert 함수에 dispatch(리듀서함수(액션객체))를 사용해보자.

우리가 받아야할 액션.페이로드는 seletedMonth와 newExes 였다. 선택한 월과 새로운 지출내역 데이터를 담는 객체를 넣어준다. 리듀서 함수 안 액션 객체 속 우리가 받아야하는 값 페이로드가 객체로 들어가는 이유는 리듀서 함수 정의할 때 객체로 받았기 때문!

const { selectedMonth, newExes } = action.payload;
const onInsert = useCallback((date, item, amount, desc) => {
    dispatch(
      addExpense({
        selectedMonth: selectedMonth,
        newExes: {
          id: uuidv4(),
          date,
          item,
          amount,
          desc,
          month: selectedMonth,
        },
      })
    );

 

dispatch로 선택된 월을 변경한다. 

 const handleMonthSelect = (idx) => {
    dispatch(setSelectedMonth(idx));
  };

 

 

 

 

 

Detail.jsx

📂 redux >  📂 slices > 📄 expense.js 

위 파일에서 삭제, 수정 리듀서 함수를 생성해준다. 삭제는 id 를 액션 페이로드로 받고, 수정은 수정된 데이터를 액션 페이로드로 받아줬다. 초기값이 객체형태였기 때문에 filter(),map() 메서드를 사용하지 못해서 for-in 문으로 객체 month를 키값으로 순회해서 배열로 가지고 왔다. 각 월 지출 내역 데이터를 expensesOfMonth 변수에 할당해주고 filter를 사용해 삭제할 데이터를 제외한 나머지 데이터들을 반환하도록 해주고 반환된 데이터를 기존 월별 데이터에 재할당해주었다. 그리고 로컬스토리지에 저장.

removeExpense: (state, action) => {
      const { id } = action.payload;
      for (const month in state.expenseList) {
        const expensesOfMonth = state.expenseList[month];
        const filteredExpenses = expensesOfMonth.filter(
          (expense) => expense.id !== id
        );
        state.expenseList[month] = filteredExpenses;
      }
      localStorage.setItem("expenseList", JSON.stringify(state.expenseList));
    },
  
  export const { addExpense, removeExpense, modifyExpense } = expense.actions;

 

수정도 마찬가지고 for-in문을 사용해주었고, map메서드를 사용해 수정하려는 데이터의 id가 기존에 있던 데이터와 같다면 기존 데이터를 수정된 데이터로 덮어준다.

 modifyExpense: (state, action) => {
      const { modifiedData } = action.payload;
      for (const month in state.expenseList) {
        const expensesOfMonth = state.expenseList[month];
        const modifyExpense = expensesOfMonth.map((ex) =>
          ex.id === modifiedData.id ? { ...ex, ...modifiedData } : ex
        );
        console.log("modifyExpense", modifyExpense);
        state.expenseList[month] = modifyExpense;
      }
      localStorage.setItem("expenseList", JSON.stringify(state.expenseList));
    },
  },

 

 

 

useSelector로 월 지출내역을 가지고와서 상세페이지에 노출시키려고 보니 오류가 발생했다. 확인해보니 내가 가지고온 데이터는 전체 월 지출내역. 상세페이지에서 필요한 데이터는 해당 월의 지출내역. 완전히 다른 데이터를 가지고 온 것이다. 그래서 가지고온 데이터를 내가 선택한 월과 같은 데이터만 가지고 오도록 find 메서드를 사용해주었다.

import { useDispatch, useSelector } from "react-redux";
import { removeExpense, modifyExpense } from "../redux/slices/expense";

const Detail = () => {
  const { id } = useParams();
  const navigate = useNavigate();
  const dispatch = useDispatch();
  const allExpenses = useSelector((state) => state.expense.expenseList);

  let exe = null;
  for (const month in allExpenses) {
    const expensesOfMonth = allExpenses[month];
    exe = expensesOfMonth.find((expense) => expense.id === id);
    if (exe) break;
  }

  const dateRef = useRef(exe.date);
  const itemRef = useRef(exe.item);
  const descRef = useRef(exe.desc);
  const amountRef = useRef(exe.amount);

  const onRemove = useCallback(() => {
    if (confirm("정말 삭제하시겠습니까?") == true) {
      dispatch(removeExpense({ id }));
      navigate("/");
    } else {
      return false;
    }
  });

  const onModify = useCallback(() => {
    dispatch(
      modifyExpense({
        modifiedData: {
          id: exe.id,
          date: dateRef.current.value,
          item: itemRef.current.value,
          amount: +amountRef.current.value,
          desc: descRef.current.value,
        },
      })
    );

    //navigate(-1);
  });

 

 

길고 길었던 코드가 짧아졌다. 프롭스 드릴링 이슈를 해결하는것 뿐만 아니라 코드도 간결하게 보여줄 수 있는 것 같다. 

const onRemove = useCallback(() => {
    if (confirm("정말 삭제하시겠습니까?") == true) {
      dispatch(removeExpense({ id }));
      navigate("/");
    } else {
      return false;
    }
  });

  const onModify = useCallback(() => {
    dispatch(
      modifyExpense({
        modifiedData: {
          id: exe.id,
          date: dateRef.current.value,
          item: itemRef.current.value,
          amount: +amountRef.current.value,
          desc: descRef.current.value,
        },
      })
    );

    //navigate(-1);
  });

리액트 첫번째 팀프로젝트가 시작됐다. supabase를 사용해서 뉴스피드를 만드는 프로젝트이다. supabase는 처음 사용해보는데 오늘 강의 들은 내용으로 정리해보고자한다.

 


 

 

 

 

Supabase❓

Supabase는 Firebase의 대안으로 나온 오픈 소스이다. Firebase와 달리 Supabase는 PostgreSQL 관계형 데이터베이스를 기반으로 관련된 다양한 기능을 제공해준다. 아래 주요 특징에 따라 가장 큰 난관인 CRUD 기능과 유저 기능을 손쉽게 해결할 수 있다.

 

Database: 확장성이 뛰어난 PostgreSQL 데이터베이스를 활용한다

Authentication: 손쉽게 소셜 로그인을 활성화할 수 있다.

Edge Functions: 서버를 배포하거나 확장하지 않고도 사용자 지정 코드를 쉽게 작성할 수 있다.

Storage: 모든 유형의 디지털 콘텐츠를 저장 및 제공한다.

 

 

 

 

Supabase 설치

yarn add @supabase/supabase-js

 

회원가입 후 프로젝트를 생성하고 데이터 베이스를 설정

https://supabase.com/dashboard/projects

 

Dashboard | Supabase

 

supabase.com

 

Project Setting -> API 에서 Project URL과 Key값을 확인할 수 있다. 

 

supabase.js 파일을 만들어 주고 위에 확인한 url과 키값을  넣어준다.

import { createClient } from '@supabase/supabase-js';

const supabaseUrl = 'URL'; 
const supabaseKey = import.meta.env.VITE_SUPABASE_KEY; 
const supabase = createClient(supabaseUrl, supabaseKey);

 

.env.local 파일 생성

VITE_SUPABASE_KEY=본인_supabase_키

 

 

 

 

인증/인가 구현하기

  • 인증
    • 증명하다
    • 내가 이 서비스의 회원이라는 것을 증명하다.
    • ex) 회원가입 & 로그인
  • 인가
    • 허가하다
    • 조회하기, 쓰기, 삭제하기, 수정하기 등을 할 수 있는 사람인지 확인하고 허가하는 것
    • ex) 로그인 후 글쓰기 가능
    • ex) 관리자 계정 → 회원관리 가능

 

로그인 구현

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'example@email.com',
  password: 'example-password',
})
const signInUser = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    setUser(data.user);
  };
  
  ...
  return
  <button onClick={signInUser}>로그인</button>

 

 

 

로그아웃 구현

const signOutUser = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.auth.signOut();
    console.log("signout: ", { data, error });
    setUser(null);
  };
  
  
  ...
  } else {
    return (
      <div>
        <p>{user.email}</p>
        <button onClick={signOutUser}>로그아웃</button>
      </div>
    );
  }

 

 

회원가입 구현

const { data, error } = await supabase.auth.signUp({
  email: 'example@email.com',
  password: 'example-password',
})
const signUpNewUser = async (e) => {
    e.preventDefault();
    const { data, error } = await supabase.auth.signUp({
      email,
      password,
    });
    setUser(data.user);
  }

 

 

 

 

 

onAuthStateChange 사용하기

Listen, subscribe → “계속 지켜본다”라는 뜻이다. useEffect에 넣어야 하는 이유는 컴포넌트 렌더링 시 한 번만 등록하면 되기 때문이다. 넣지 않으면 리렌더링마다 계속 CCTV 추가가 된다. 하나만 있으면 되는데 계속 추가되면 성능 안좋아질 수 있다

useEffect(() => {
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      if (session) {
        setUser(session.user);
      } else {
        setUser(null);
      }
    });

    return () => subscription.unsubscribe();
  }, []);

 

props-drilling에 이어 context로 props를 전달해서 기존 기능들이 정상적으로 작동되게 만들어보자!!🔥

 

 

 

📌 props-drilling으로 만든 지출 관리 사이트

https://ejunyang.tistory.com/entry/%EB%A6%AC%EC%95%A1%ED%8A%B8-%EA%B0%9C%EC%9D%B8-%EC%A7%80%EC%B6%9C-%EA%B4%80%EB%A6%AC-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EB%A7%B9%EB%93%A4%EA%B8%B0%F0%9F%90%A2

 

[리액트] 개인 지출 관리 사이트 맹들기🐢

세번째 개인 프로젝트이다. 리액트로는 두번째 개인 프로젝트! 간단한 개인 지출 관리 사이트를 만들어보자!!🐢이번 과제는 props-drilling > context > redux 순으로 상태 관리를 변경해가며 구현해야

ejunyang.tistory.com

 

 

컴포넌트

컴포넌트 설명
ExesForm.jsx 지출내역을 입력하는 컴포넌트이다. 메인 페이지 우측 하단 버튼을 누르면 모달처럼 뜨도록 구현했다.
MonthList.jsx 월별 버튼 컴포넌트이다. 해당 월을 누를 때마다 다른 스타일링이 들어간다.
ExesList.jsx 월별 버튼을 눌렀을 때 하단에 해당 월의 지출내역이 나타나는 지출 내역 리스트 컴포넌트이다.
ExesItem.jsx 각 지출 내역의 컴포넌트이다.

 

 


 

 

컨텍스트 생성

📂 context >  📂 Context.jsx

import { createContext } from "react";

export const Context = createContext(null);

 

 

App.jsx

최상위 컴포넌트에서 각 컴포넌트에서 사용할 props를 Provider를 사용해 전달해준다. value로 전달해주기 위해선 App.jsx에 데이터들이 있어야한다.

import { useCallback, useEffect, useState } from "react";
import { Context } from "./context/Context";

return (
    <Context.Provider
      value={{
        exes,
        setExes,
        selectedMonth,
        setSelectedMonth,
        handleMonthSelect,
        onInsert,
        filteredList,
      }}
    >
      <Router />
    </Context.Provider>
  );
};

export default App;

 

 

ExesForm.jsx

 

useContext를 사용해서 사용하고 있던 데이터를 가져온다. 지출내역을 추가하는 컴포넌트로 onInsert 함수를 가져온다.

import React, { useCallback, useContext, useState } from "react";
import { FaPen } from "react-icons/fa6";
import styled from "styled-components";
import { Context } from "../context/Context";

const ExesForm = () => {
  const { onInsert } = useContext(Context);

  const onSubmit = useCallback(
    (e) => {
      ...
    },
    [date, item, amount, desc]
  );

  return (
   ...
  );
};

export default ExesForm;

 

 

 

ExesList.jsx

여기서 살짝 헤맸는데 해당 지출 내역을 가져와서 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;

 

 

MonthList.jsx

import React, { useContext } from "react";
import "../App.css";
import styled from "styled-components";
import { Context } from "../context/Context";

const MonthList = () => {
  const { exes, selectedMonth, handleMonthSelect } = useContext(Context);

  const monthArray = [1,2,3,4,5,6,7,8,9,10,11,12];

  const totalAmount =
    exes[selectedMonth] && exes[selectedMonth].length > 0
      ? exes[selectedMonth].reduce((acc, cur) => {
          return (acc += cur.amount);
        }, 0)
      : 0;

  return (
    <>
     ...
    </>
  );
};

export default MonthList;

 

 

 

Detail.jsx

 

props-drilling으로 작업할 때 디테일 페이지에 지출내역의 setState를 가져오지 못해서 상태를 업데이트해주지 못했는데 context를 사용하면 setState 가져올 수 있다!

import React, { useCallback, useContext, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
import styled from "styled-components";
import { IoIosArrowBack } from "react-icons/io";
import { Context } from "../context/Context";

const Detail = () => {
  const { id } = useParams(); 
  const navigate = useNavigate(); 
  const { filteredList, setExes } = useContext(Context);

  const exe = filteredList.find((item) => item.id === id);

  return (
    <>
      ...
    </>
  );
};

export default Detail;

 

삭제할 지출 내역을 제외한 새로운 배열을 생성하고,  그 배열을 포함한 새로운 객체를 만든다. 불변성 유지를 위해 전체 월 지출내역 복사,  removeExes 안에는 전체 지출 내역 중 삭제된 지출내역을 제외한 나머지 지출내역이 들어가 있다. 로컬스토리지에 해당 데이터를 저장하고 setExes(지출 내역 업데이트)로 상태 업데이트를 해준다.

const onRemove = useCallback(
    (id) => {
      if (confirm("정말 삭제하시겠습니까?") == true) {
        const removeExes = {
          ...data,
          [exe.month]: data[exe.month].filter((ex) => ex.id !== id),
        };
        localStorage.setItem("expenseList", JSON.stringify(removeExes));
        setExes(removeExes);
        navigate("/");
      } else {
        return false;
      }
    },
    [exe]
  );

 

수정 기능도 불변성 유지를 위해 이전 데이터를 복사 후에 수정된 데이터 배열을 월 지출 내역에 업데이트 해준다. 

const onModify = useCallback(
    (id) => {
      const modifiedData = {
        id: exe.id,
        date: dateRef.current.value, //변수 접근
        item: itemRef.current.value,
        amount: +amountRef.current.value, //숫자로만 받아야하기때문에 +연산자 사용(NaN 오류 발생)
        desc: descRef.current.value,
      };
      data[exe.month] = data[exe.month].map((ex) =>
        ex.id === id ? { ...ex, ...modifiedData } : ex
      );
      setExes((prevExe) => ({
        ...prevExe,
        [exe.month]: data[exe.month],
      }));
      localStorage.setItem("expenseList", JSON.stringify(data));
      alert("수정이 완료되었습니다.");
    },
    [exe]
  );

 

세번째 개인 프로젝트이다. 리액트로는 두번째 개인 프로젝트! 간단한 개인 지출 관리 사이트를 만들어보자!!🐢

이번 과제는 props-drilling > context > redux 순으로 상태 관리를 변경해가며 구현해야한다. 과제를 진행하면서 props-drilling의 단점과 전역 상태 관리 라이브러리의 장점을 이해하도록 하려는 취지 같았다. 결론적으론 전역 상태 라이브러리를 사용하면 props-drilling 이슈를 해결할 수 있다는 것만큼은 제대로 알았다. 

 

 


 

 

와이어 프레임

 

참고로 준 프로젝트 완성본은 맨 위에 입력 폼이 있는데 디자인은 달리 들어가도된다고 해서 따로 디자인하고 작업했다. 아래와 같이 컴포넌트 분리를 했고 react-router-dom으로 페이지 분리를 먼저 해주려고 한다.

 

 

컴포넌트

컴포넌트 설명
ExesForm.jsx 지출내역을 입력하는 컴포넌트이다. 메인 페이지 우측 하단 버튼을 누르면 모달처럼 뜨도록 구현했다.
MonthList.jsx 월별 버튼 컴포넌트이다. 해당 월을 누를 때마다 다른 스타일링이 들어간다.
ExesList.jsx 월별 버튼을 눌렀을 때 하단에 해당 월의 지출내역이 나타나는 지출 내역 리스트 컴포넌트이다.
ExesItem.jsx 각 지출 내역의 컴포넌트이다.

 

 


 

 

 

 

라우터 설정

yarn add react-router-dom

 

 

📂 shared >  📂 Router.jsx

react는 SPA(Single page application)이며 따라서 하나의 프로젝트에 하나의 html만 존재한다. 업데이트 되는 새로운 페이지만 path와 element로 연결하여 컴포넌트를 불러오는 방식이다. 고유한 id 값으로 이동할 수 있도록 설정했다. BrowserRouter로  Router을 감싸는 이유는 브라우저가 깜빡거리지 않고 이동할 수 있도록 해준다.

import React, { useState } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "../pages/Home";
import Detail from "../pages/Detail";
import Layout from "./Layout";

const Router = () => {
  return (
    <div>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Layout />}>
            <Route index element={<Home />} />
            <Route path="/detail/:id" element={<Detail />} />
          </Route>
        </Routes>
      </BrowserRouter>
    </div>
  );
};
export default Router;

 

 

📂 pages >  📂 Home.jsx

아래처럼 컴포넌트를 배치했다. 프로젝트 필수 사항에 styled-components를 사용하라고해서 모든 컴포넌트에 사용했는데 css 파일로 따로 가지 않고 js파일에서 수정할 수 있고, 조건부 스타일링이 가능한 점은 편리하고 좋지만, 컴포넌트가 많아보이고 헷갈린다.. 장점이 큰 만큼 단점도 만만치않게 큰 것 같다.

 return (
    <>
      <StContainer>
        <MonthList
          key={exes.id}
          exes={exes}
          selectedMonth={selectedMonth}
          handleMonthSelect={handleMonthSelect}
        />
        <ExesList key={exes.id} filteredList={filteredList} setExes={setExes} />
        <ExesForm onInsert={onInsert} exes={exes} />
      </StContainer>
    </>
  );
};

export default Home;

 

 

 

ExesForm.jsx

지출 내역을 입력하는 컴포넌트다. 새로운 지출내역을 추가하는 객체를 만들어준다. 지출내역엔 날짜와 항목 지출내용과 금액이 들어간다. 추가적으로 month키를 넣어주었는데 해당 키로 지출 내역이 해당하는 월을 확인할 수 있다.

✔️ 등록

const onInsert = (date, item, amount, desc) => {
    const newExes = {
      id: uuidv4(),
      date,
      item,
      amount,
      desc,
      month: selectedMonth,
    };
  };

 

월마다 지출내역이 달리 보여야하기 때문에 useState로 상태를 만들어주었다. 아래는 현재 날짜로 임의로 설정해주었는데 이는 로컬 스토리지를 사용하여 마지막으로 선택된 '월'을 저장하고, 해당 페이지가 다시 시작 될 때마다 해당 값을 불러오도록 하려고한다.

const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());

 

불변성 유지를 위해 기존 지출 내역 객체를 복사해준다. addExes[selectedMonth]는 선택한 월의 지출내역 데이터이다. 해당 월의 지출내역이 없다면 빈배열을 가져오도록 하고, 새로운 지출내역은 해당 월 지출 내역에 추가해준다.

const addExes = { ..exes };
    if (!addExes[selectedMonth]) {
      addExes[selectedMonth] = [];
    }
    addExes[selectedMonth].push(newExes);
    setExes(addExes);

 

해당 월의 지출내역 데이터를 fiteredList 변수에 할당한다. 이 변수는 선택한 월에 해당하는 지출 내역을 보여주는 ExesList에 props로 내려준다. 필터링된 데이터로 지출 아이템을 하나씩 만들어줘야하기 때문!

const filteredList = exes[selectedMonth];

 

 

✔️ 로컬스토리지

지출 내역 초기값을 설정해준다. 

const initalLocalData = localStorage.getItem("expenseList")
    ? JSON.parse(localStorage.getItem("expenseList"))
    : {};

 

마지막으로 확인한 월 저장.

const initialSelectedMonth = localStorage.getItem("selectedMonth")
    ? parseInt(localStorage.getItem("selectedMonth"))
    : new Date().getMonth() + 1;

 

지출 내역과 월 초기값 상태.

const [exes, setExes] = useState(initalLocalData);
const [selectedMonth, setSelectedMonth] = useState(initialSelectedMonth);

 

데이터 저장. 

localStorage.setItem("expenseList", JSON.stringify(addExes));

 useEffect(() => {
    localStorage.setItem("selectedMonth", selectedMonth);
  }, [selectedMonth]);

 

 

 

 

ExesList.jsx

선택한 월에 해당하는 지출 내역을 보여주는 리스트 컴포넌트이다.

const ExesList = ({ filteredList }) => {
  return (
    <div>
      <ul>
        {filteredList &&
          filteredList.map((exe) => {
            return <ExesItem key={exe.id} exe={exe} />;
          })}
      </ul>
    </div>
  );
};

export default ExesList;

 

 

 

ExesItem.jsx

필터링된 데이터로 각 지출 내역을 보여주는 컴포넌트이다. 아이템을 눌렀을 때 해당하는 지출의 상세페이지로 이동해야하기 때문에(Detail.jsx) useNavigate() 훅을 사용해주었다. 

const ExesItem = ({ exe }) => {
  const navigate = useNavigate();
  const { id, date, item, amount, desc } = exe;

  const onDetailButtonHandler = (id) => {
    navigate(`/detail/${id}`, { state: { exe } });
  };
  return (
    <>
      <ExeItem onClick={() => onDetailButtonHandler(id)}>
        <ExeHead>{item}</ExeHead>
        <ExeItemP>
          {desc}
          <Sapn>{date}</Sapn>
        </ExeItemP>
        <H2>{amount}원</H2>
      </ExeItem>
    </>
  );
};

export default ExesItem;

 

 

 

 

MonthList.jsx

월별 버튼 컴포넌트이다. 1-12월까지 해당하는 버튼을 눌렀을 때 선택된 스타일링이 들어가야한다. 월별 버튼을 구현할 때 필수사항이었던 style-component를 사용해 조건부 스타일링을 해주었다.

const MonthList = ({ handleMonthSelect }) => {
  const monthArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];

  return (
    <>
      <MonthWrap>
        {monthArray.map((MonthText, idx) => {
          return (
            <MonthItem
              key={idx}
              $active={selectedMonth === idx}
              onClick={() => handleMonthSelect(idx)}
            >
              {`${MonthText}월`}
            </MonthItem>
          );
        })}
      </MonthWrap>
    </>
  );
};

export default MonthList;

 

 

월이 들어가있는 배열을 순회하면서 $active props에 선택한 월과 배열에 있는 인덱스가 같다면 스타일링을 줄 수 있도록 했다.

{monthArray.map((MonthText, idx) => {
          return (
            <MonthItem
              key={idx}
              $active={selectedMonth === idx}
              onClick={() => handleMonthSelect(idx)}
            >
              {`${MonthText}월`}
            </MonthItem>
          );
        })}

 

삼항 연산자로 처리했다. props로 $active가 있다면 색상을 파란색으로 바꾸고 아니라면 회색(디폴트)색으로 변경. 폰트 색상도 동일하게 조건부 스타일링을 해주었다.

background-color: ${(props) => (props.$active ? "#89ACEC" : "#f4f5f7")};
color: ${(props) => (props.$active ? "#fff" : "#2e2e2e")};

 

상태 업테이트까지 하면 완성.

const handleMonthSelect = (idx) => {
    setSelectedMonth(idx);
  };

 

 

Detail.jsx

각 지출 내역의 상세페이지 컴포넌트이다. props-drilling으로 만들면서 디테일 페이지에는 어떻게 props를 전달해줘야하나 고민을 많이 했다.. 구글링을 하면서 useLocation으로 가져오는 방법을 찾아서 전체 지출 내역 리스트를 가져와서 사용해봤다.

const Detail = () => {
  const { id } = useParams();
  const navigate = useNavigate(); 
  const location = useLocation(); 
  const exe = location.state.exe;

  return (
    <>
      <StContainer>
        <TopWrap>
          <IoIosArrowBack
            style={{ fontSize: "25px", cursor: "pointer" }}
            onClick={onBackButtonHandler}
          />
          <H1>상세 내역</H1>
          <IoIosArrowBack style={{ fontSize: "25px", opacity: "0" }} />
        </TopWrap>

        <ExeItem>
          <ExeHead>{exe.item}</ExeHead>
          <ExeItemP>
            {exe.desc} <Sapn>{exe.date}</Sapn>
          </ExeItemP>
          <H2>{exe.amount}원</H2>
        </ExeItem>

        <DetailContainer>
          <label>날짜</label>
          <StInput ref={dateRef} defaultValue={exe.date} />
          <label>항목</label>
          <StInput ref={itemRef} defaultValue={exe.item} />
          <label>내용</label>
          <StInput ref={descRef} defaultValue={exe.desc} />
          <label>금액</label>
          <StInput ref={amountRef} defaultValue={+exe.amount} />

          <ButtonWrap>
            <EditButton onClick={() => onModify(id)}>수정</EditButton>
            <RemoveButton onClick={() => onRemove(id)}>삭제</RemoveButton>
          </ButtonWrap>
        </DetailContainer>
      </StContainer>
    </>
  );
};

export default Detail;

 

 

✔️ 삭제

로컬스토리지에 저장된 데이터를 삭제하고 업데이트하는 로직으로 구현했다. 로컬스토리지에 저장되어있는 전체 지출 내역을 data 변수에 할당해준다. 

let data = JSON.parse(localStorage.getItem("expenseList"));

 

삭제 버튼 클릭 시 즉시 삭제하기 보다는 사용자에게 확인받은 뒤 삭제처리하도록 했다. data[exe.month]는 해당하는 월의 지출내역을 담고있다. 배열이기때문에 filter 메서드를 사용해 삭제하려는 데이터는 제외한 나머지 데이터들을 removeData 변수에 할당해주었다.

그리고 여기서 해당하는 월의 지출내역에 다시 재할당 해준다. 아무리 구글링해도 setState는 props로 가져올수가 없어서 상태를 업데이트 해주는 대신 저장된 데이터를 업데이트해주는 로직으로 구현한 것이다..

const onRemove = (id) => {
    if (confirm("정말 삭제하시겠습니까?") == true) {
      const removeData = data[exe.month].filter((ex) => ex.id !== id);
      data[exe.month] = removeData;
      navigate(-1);
    } else {
      return false;
    }
  };

 

 

 

✔️ 수정

useRef를 사용해서 수정 기능을 구현했다. 리렌더링되도 초기값이 변하지 않는다. 

  const dateRef = useRef(exe.date); //초기값
  const itemRef = useRef(exe.item);
  const descRef = useRef(exe.desc);
  const amountRef = useRef(exe.amount);

 

이것도 삭제와 같은 방법으로 상태 업데이트가 아니라 저장된 로컬스토리지 데이터에 수정된 데이터를 덮어씌우는 로직으로 갔다. 

수정하려는 객체를 하나 만들어주고 변수명.current.value로 수정하려는 데이터에 접근할 수 있다. 해당 월 지출 내역을 순회하면서 수정하려는 데이터의 id가 해당 월 지출내역의 id가 같다면 기존 지출내역을 복사하고 수정된 데이터도 복사해 합쳐준다. 

const onModify = (id) => {
    const modifiedData = {
      id: exe.id,
      date: dateRef.current.value,
      item: itemRef.current.value,
      amount: +amountRef.current.value,
      desc: descRef.current.value,
    };
    const modifyExes = data[exe.month].map((ex) =>
      ex.id === id ? { ...ex, ...modifiedData } : ex
    );
    data[exe.month] = modifyExes;
    localStorage.setItem("expenseList", JSON.stringify(data));
    navigate(-1);
  };

 

처음 짠 코드는 아래와 같다. 이렇게 했더니 정상적으로 동작하지 않았다. 기존에 있는 데이터는 month 값이 있지만 수정한 객체는 month 값이 들어가 있지 않아서 생긴 오류였다. 그래서 위와 같이 스프레드 연산자를 사용해 불변성을 유지하고 기존 데이터에 있는 month의 값도 들어갈 수 있었다. 

const modifyExes = data[exe.month].map((ex) =>
      ex.id === id ? modifiedData : ex
    );

 

 

 

 

memoization❓

함수의 결과를 캐싱하여 동일한 입력에 대해 다시 계산하지 않도록 하는 최적화 기법이다. 이전의 계산한 값을 메모리에 저장하고, 중복된 계산을 제외하고 실행속도를 빠르게 해주는 기법이다.

 

캐싱 : 자주 사용하는 데이터를 임시로 저장해 두고, 필요할 때 빠르게 접근할 수 있도록 하는 것

 

 


 

 

 

 

React.memo

컴포넌트를 캐싱해서 부모 컴포넌트 리렌더링 시 캐싱된 컴포넌트는 리렌더링하지 않고 재사용하는 API이다.

이때 props를 받는 컴포넌트는 자식 컴포넌트이다. 부모 컴포넌트가 리렌더링 되면 자식 컴포넌트는 props 상태가 변하지 않더라도 리렌더링된다. 불필요한 리렌더링을 막기 위한 방법을 해결하기 위해 React.memo를 사용하는 것이다.

 

아래 코드를 예시로 보자면 memo 를 사용하지 않았을 때는 input창에 값을 입력할 때마다 console.log("List component re-rendered"); 이 실행된다. 이는 부모컴포넌트에서 일어난 상태 변화에 자식 컴포넌트는 변화가 없음에도 불구하고 계속해서 리렌더링된다는 뜻이다. 

import { useState } from "react";

const List = (({ items }) => {
  console.log("List component re-rendered");
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
});

const App = () => {
  const [text, setText] = useState("");
  const [items, setItems] = useState([]);

  const handleAdd = () => {
    setItems([...items, text]);
    setText("");
  };

  return (
    <div>
      <input
	      type="text"
	      value={text}
	      onChange={e => setText(e.target.value)}
	      placeholder="Add item"
	    />
      <button onClick={handleAdd}>Add</button>
      <List items={items} />
    </div>
  );
};

export default App;

 

처음 렌더링된 후 콘솔이 한번 찍히고 입력창에 값을 입력할 때마다 리렌더링이 된다는 것을 확인할 수 있다. 

 

 

 

이때 React.memo를 사용해보면 차이를 확인할 수 있다. memo의 인자로 List라는 함수 컴포넌트를 넣었을 때 이전에 저장한 값을 불러와 리렌더링하지 않고 재사용할 수 있다.

import { useState, memo } from "react";

const List = memo(({ items }) => {
  console.log("List component re-rendered");
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
});

 

처음 렌더링될 때만 실행되고 입력창에 값을 넣어도 리렌더링되지 않는 것을 확인할 수 있다. 

 

 

 

 

 

 

useMemo

계산 결과 값을 캐싱해서 리렌더링 시 재사용할 수 있는 훅이다. 

useMemo는 자주 쓰이는 캐싱해준다. 그리고 그 값이 필요할때 다시 계산을 하는 것이 아닌 useMemo를 통해 캐싱을 한 값을 메모리에서 꺼내와서 재사용한다.

 

 

기본문법

아래 코드와 같이 useMemo는 인자로 함수와 의존 값(dependencies)를 받는다.

const cachedValue = useMemo(calculateValue, dependencies)

 

 

매개변수

calculateValue 캐시하려는 값을 계산하는 함수. 인자를 받지 않고, 반드시 어떤 타입이든 값을 반환해야한다. 의존성이 이전 렌더링 이후 변경되지 않았다면 동일한 값을 반환합니다. 그렇지 않으면 calculateValue를 호출하고 그 결과를 반환하며, 나중에 재사용할 수 있도록 저장한다.
dependencies React는 마지막 렌더링 이후에 의존 인자 중 하나의 값이라도 변경되면 calculateValue를 호출하여 값을 재계산한 리턴값을 반환한다. 의존 인자가 변경되지 않은 경우에는 동일한 값을 다시 반환한다.

 

 

 

아래 예시를 보면 컴포넌트가 렌더링될 때마다 value의 값이 초기화 된다. 렌더링이 될 때마다 불필요하게 재호출된다는 뜻이다. 이를 해결하기 위해 useMemo를 사용해보자.

function Component() {
    const value = calculate();
    return <div>{value}</div> 
}

function calculate() {
    return 10;
}

 

 

useMemo를 사용한 코드이다. useMemo도 useEffect처럼 첫번째 인자에 콜백함수, 두번째 인자엔 의존 인자를 가지고 있다. 의존 인자의 값이 변경될 때 콜백함수를 실행하여 값을 업데이트해준다. 만약 빈 배열을 넣는다면 useEffect와 마찬가지로 마운트 될 때에만 값을 계산하고 그 이후론 계속 캐싱된 값을 꺼내와 사용한다.

function Component() {
    const value = useMemo(() => {
    	return calculate();
    },[item])
    
    return <div>{value}</div> 
}

 

 

 

 

useCallback

함수 정의를 캐싱 해서 리렌더링 시 재사용할 수 있는 훅이다.

 

기본문법

const cachedFn = useCallback(fn, dependencies)

 

 

매개변수

fn 캐싱할 함숫값. 이 함수는 어떤 인자나 반환값도 가질 수 있다
dependencies fn 내에서 참조되는 모든 반응형 값의 목록. 반응형 값은 props와 state, 그리고 컴포넌트 안에서 직접 선언된 모든 변수와 함수를 포함

 

 

 

아래 코드는 컴포넌트가 렌더링할 때마다 handleIncrement 함수를 재생성한다. 렌더링될 때마다 같은 동작을 하지만, 함수의 참조가 변경되므로 React는 매 렌더링 시마다 새로운 함수로 인식하게된다. 이때 useCallback 훅을 사용해서 함수가 재생성되는 것을 해결할 수 있다.

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

const MyComponent = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    setCount(prevCount => prevCount + 1);
  }

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

 

 

첫번째 인자에는 콜백함수를 넣고, 두번째 인자인 의존성 배열에는 count를 넣어 해당 값이 변경될 때마다 실행하도록 한다. 이렇게하면 렌더링될 때마다 함수가 재생성되는 것을 해결할 수 있으며, count 값이 변경되기 전 까진 이전에 캐싱한 함수를 재사용할 수 있다.

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

const MyComponent = () => {
  const [count, setCount] = useState(0);

  const handleIncrement = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>Increment</button>
    </div>
  );
};

 

 

Redux❓

리덕스는 자바스크립트 앱의 상태를 관리하기 위한 전역 상태 관리 라이브러리이다. 

 

 

Redux를 사용하는 이유❓

리덕스를 사용하면 상태값을, 컴포넌트에 종속시키지 않고, 상태 관리를 컴포넌트의 바깥에서 관리 할 수 있게 된다.

 

 

👾 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 });

 

 

조합한 리듀서로 스토어를 생성한다.

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)을 합쳐서 새로운 배열을 생성한다.

// 초기값 설정
const initialState = {
  text: localStorage.getItem("texts") || [],
};
console.log(initialState);

const texts = (state = initialState, action) => {
  switch (action.type) {
    case "onAddText":
      return {
        ...state,
        text: [...state.text, action.payload],
      };
    default:
      return state;
  }
};

export default texts;

 

 

 

 

리스트를 추가할 때 input창에 입력하는 값이 출력되어야한다. 리듀서 함수에서 만든 액션 타입을 dispatch 메서드를 사용해서 스토어에 상태 변경을 요청하는 방법은 아래와 같다. 

import React, { useState } from "react";
import { useDispatch } from "react-redux";

function TextInput() {
  const dispatch = useDispatch();
  const [inputValue, setInputValue] = useState("");

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      dispatch({ type: "onAddText", payload: inputValue });
      setInputValue("");
    }
  };

  return (
   <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={inputValue}
        onChange={handleChange}
        placeholder="Enter text"
      />
      <button type="submit">Add</button>
    </form>
  );
}

export default TextInput;

 

 

useContext 

useContext는 컴포넌트에서 context를 읽고 구독할 수 있게 해주는 React Hook입니다.

 

 

기본구문

const value = useContext(SomeContext)

 

매개변수

SomeContext : 이전에 createContext로 생성한 context입니다. context 자체는 정보를 보유하지 않으며, 컴포넌트에서 제공하거나 읽을 수 있는 정보의 종류를 나타낼 뿐입니다.

 

반환값

호출한 컴포넌트에서 트리상 위에 있는 가장 가까운 SomeContext.Provider에 전달된 value입니다. 이러한 provider가 없는 경우 반환되는 값은 해당 context에 대해 createContext에 전달한 defaultValue가 됩니다. 반환된 값은 항상 최신 값입니다. React는 context가 변경되면 context를 읽는 컴포넌트를 자동으로 리렌더링합니다.

 

 


 

 

 

React Context의 필요성

일반적으로 부모컴포넌트에서 자식 컴포넌트에게 데이터를 전달해줄때 props로 전달해준다. 하지만 큰 단점이 있는데 컴포넌트가 깊어질수록 해당 컴포넌트가 어느 컴포넌트에서 왔는지 파악하기 어려워진다. 이것을 props-drilling이라고 한다.

 

prop drilling의 문제점

  1. 깊이가 너무 깊어지면 이 prop이 어떤 컴포넌트로부터 왔는지 파악이 어려워진다.
  2. 어떤 컴포넌트에서 오류가 발생할 경우 추적이 힘들다.

 

 

Props 사용이 적절할 때❓

리스트 컴포넌트에서 아이템 컴포넌트로 상태를 전달하는 경우. 이 경우는 이번 프로젝트를 하면서 많이 생소했다. props drilling으로 만든 코드를 context 로 변경하는 과정에서 리스트 컴포넌트에는 도저히 context를 사용할 수 없을 것 같아서였다. map을 돌린 애들로 아이템을 만드는데 여기서 context를 어떻게 사용할까 했는데 오늘 강의 때 단순한 데이터 전달은 Props 사용이 적절한 때라고 알려주셔서 이해할 수 있었다.

export default function TodoList({ isDone }) {
  const { todos, setTodos } = useContext(TodoContext);
  const filteredTodos = todos.filter((todo) => todo.isDone === isDone);
  return (
    <div>
      <h2>{isDone ? "Done" : "Working..."}</h2>
      {filteredTodos.map((todo) => (
        <TodoItem key={todo.id} todo={todo} setTodos={setTodos} />
      ))}
    </div>
  );
}
export default function TodoListWrapper() {
    return (
        <>
            <TodoList isDone={false} />
            <TodoList isDone={true} />
        </>
    );
}

 

 

Context API

아래 그림처럼 useContext hook을 통해 우리는 쉽게 전역 데이터를 관리할 수 있다. Context Provider 의 모든 children 컴포넌트들에게 context value를 공유할 수 있고, 전역상태관리를 하게 되면 props 를 이용한 상태 공유를 하지 않아도 된다.

 

  • createContext : context를 생성
  • useContext : context를 구독하고 해당 context의 현재 값을 읽기
  • Provider : context를 하위 컴포넌트에게 전달

 

 

 

 

createContext

Context Provider 의 모든 children 컴포넌트들에게 context value를 공유할 수 있다. 

const TodoContext = createContext(null);

 

 

Provider

 context를 하위 컴포넌트에게 전달한다.

function App () {
	const [todos, setTodos] = useState([]);
	
	return (
		<TodoContext.Provider value={{todos, setTodos}}>
			<TodoForm />
			<TodoListWrapper />
		</TodoContext.Provider>	
	);
}

 

예시

// src/context/TodoContextProvider.jsx
import React, { createContext, useState } from 'react';

// 함수 컴포넌트 외부에서 Context 생성
export const TodoContext = createContext(null);

const TodoContextProvider = ({ children }) => {
  const [todos, setTodos] = useState([]);

  const addTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text, isDone: false }]);
  };

  return (
    <TodoContext.Provider value={{ todos, addTodo }}>
      {children}
    </TodoContext.Provider>
  );
};

export default TodosContextProvider;

 

useContext

context를 구독하고 해당 context의 현재 값을 읽어올 수 있다. useContext() 괄호 안은 리턴 + 구독을 뜻한다. 아래 코드는 todoContext를 구독하겠다는 말과 같다.

function TodoForm() {
	const { setTodos } = useContext(TodoContext);
	return (
		...
	)
}

 

 

 

실습을 하던 중에 디테일 페이지에 데이터를 받아야하는데 부모 컴포넌트와 종속성이 없어 데이터를 받아오지 못하는 상황이 있었다. 라우터 간 데이터 전달은 context api로 할 수 있는데 props-drilling으로 작업하고 있어서 어찌해야할 방법을 몰랐다. 그런데 설마 했던 방법이 맞았다..! 라우터 컴포넌트에서 상태를 전달하는 것..! 너무나도 생소해서 여긴 아니겠지 하는 생각으로 갈피를 잡지 못했었다. 

// 여러 페이지 컴포넌트에 동일한 상태 공유가 필요할 때
function Router() {
	return (
	<BrowserRouter>
	    <Routes>
        	<Route path="/" element={<Home />} />
        	<Route path="/detail/id" element={<Detail />} />
	    </Routes>
	</BrowserRouter>		
	)
}

 

Route.jsx에다 상태를 만들어주고 props로 내려주면 되는것.. 방법을 못찾아서 useLocation으로 데이터를 받아왔었는데 props-drilling의 불편함을 어느정도 알 수 있게해준 오류였다.

function Router() {
const [todos, setTodos] = useState([]);
	return (
	<BrowserRouter>
	    <Routes>
        	<Route path="/" element={<Home />} />
        	<Route path="/detail/id" element={<Detail todos={todos}/>} />
	    </Routes>
	</BrowserRouter>		
	)
}

 

+ Recent posts