77일차 (1) - React (useState, 상태관리: 단일값vs객체, Props Drilling, 동적 리스트 렌더링)

Yohan·2024년 6월 13일
0

코딩기록

목록 보기
118/156
post-custom-banner

상태값과 useState

  • 상태값은 사용자의 입력, API 응답 등으로 인해 시간에 따라 변할 수 있는 값
  • 상태는 컴포넌트의 렌더링 결과를 결정하는 요소 중 하나로, 상태가 변경되면 컴포넌트는 재렌더링됨
  • react에서는 useState의 setter를 사용해야 변경상태 감지
    -> 상태값을 직접 변경하면, React는 상태가 변경되었는지 알 수 없으며, 이로 인해 컴포넌트가 적절하게 업데이트되지 않을 수 있다. 따라서 상태를 변경할 때는 항상 setter함수를 사용해야 한다.
  • useState의 첫번째 요소는 관리할 상태값의 초기값, 두번째 요소는 해당 상태값을 변경할 때 사용하는 setter 함수

useState의 setter의 특징

useState의 setter는 상태값을 업데이트할 때 렌더링 전까지 이전 상태값을 참조함

  • 해결방법: 함수형 업데이트를 사용.
    setCount((prev) => {
      console.log('변경 이전값: ', prev);
      // 변경 이후값을 반환
      return prev + 1;
    });
    setCount(count => count + 1);
  • 현재 값이 3이라면 prev = 3이다. 콜백에서 prev + 1을 리턴하고 있으므로 여기서 prev가 4가된다. 그리고 count는 4가되고 여기서도 1이 늘어나므로 결과적으로 3 -> 5로 되어 2씩 늘어나게 해주는 것을 알 수 있다.

React 상태 관리: 단일 값 vs 객체

1. 단일 값 상태: 상태의 각 부분을 별도의 상태로 관리하는 방식

import React, { useState } from 'react';
import './ExpenseForm.css';

const ExpenseForm = () => {

  // 입력칸에 있는 3개의 값을 각각의 상태값으로 관리
  const [title, setTitle] = useState('');
  const [price, setPrice] = useState(0);
  const [date, setDate] = useState(null);

  // 오늘 날짜를 YYYY-MM-DD 형식으로 가져오는 함수
  const getTodayDate = () => {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 1을 더해줌
    const day = String(today.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  // 제목이 입력되었을 때 발생하는 이벤트 핸들러
  const titleChangeHandler = e => {
    setTitle(e.target.value); // 입력한 값으로 변경
  };

  // 가격이 입력되었을 때 발생하는 이벤트 핸들러
  const priceChangeHandler = e => {
    setPrice(+e.target.value);
  };

  // 날짜가 입력되었을 때 발생하는 이벤트 핸들러
  const dateChangeHandler = e => {
    setDate(e.target.value);
  };

  // 폼 전송 이벤트 핸들러
  // type이 submit이면 button에 onClick 대신 form에 onSubmit으로 가능
  const submitHandler = e => {
    e.preventDefault(); // 폼 전송 방지

    // 지출 내역 객체를 생성
    const newExpense = {
      title,
      price,
      date
    };

    console.log(newExpense);
  };

  return (
    <form onSubmit={submitHandler}>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input type="text" onChange={titleChangeHandler} />
        </div>
        <div className="new-expense__control">
          <label>Price</label>
          <input
            type="number"
            min="100"
            step="100"
            onChange={priceChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            max={getTodayDate()}
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

2. 객체 상태: 여러 상태를 하나의 객체로 묶어 관리하는 방식

객체나 배열상태로 관리되는 상태값은 상태변경시 새로운 객체나 배열을 setter에 전달해야 함 -> 리액트는 상태변경시 무조건 새 객체에 넣어서 전달해함

import React, { useState } from 'react';
import './ExpenseForm.css';

const ExpenseForm = () => {
  
  // 입력칸에 있는 3개의 값을 하나의 상태값으로 관리 (객체로 관리)
  const [userInput, setUserInput] = useState({
    title: '',
    price: '',
    date: ''
  });

  // 오늘 날짜를 YYYY-MM-DD 형식으로 가져오는 함수
  const getTodayDate = () => {
    const today = new Date();
    const year = today.getFullYear();
    const month = String(today.getMonth() + 1).padStart(2, '0'); // 월은 0부터 시작하므로 1을 더해줌
    const day = String(today.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  // 제목이 입력되었을 때 발생하는 이벤트 핸들러
  const titleChangeHandler = e => {

    // userInput.title = e.target.value; (X)

    // 객체나 배열상태로 관리되는 상태값은
    // 상태변경시 새로운 객체나 배열을 setter에 전달해야 함
    // -> 리액트는 상태변경시 새 객체를 넣어주어야함
    setUserInput(prevUserInput => ({
      ...prevUserInput, // 기존에 있던 데이터는 복사
      title: e.target.value
    }));

    
  };

  // 가격이 입력되었을 때 발생하는 이벤트 핸들러
  const priceChangeHandler = e => {
    setUserInput({
      ...userInput,
      price: +e.target.value
    })
  };

  // 날짜가 입력되었을 때 발생하는 이벤트 핸들러
  const dateChangeHandler = e => {
    setUserInput({
      ...userInput,
      date: e.target.value
    })
  };
 
  // 폼 전송 이벤트 핸들러
  // type이 submit이면 button에 onClick 대신 form에 onSubmit으로 가능
  const submitHandler = e => {
    e.preventDefault(); // 폼 전송 방지

    // 지출 내역 객체를 생성
    // -> 객체로 관리시 애초에 객체를 생성했기 때문에 안해도됨
    // const newExpense = {
    //   title,
    //   price,
    //   date
    // };

    console.log(userInput);

    // form input 비우기
    setUserInput({
      title: '',
      price: '',
      date: ''
    });
  };

  return (
    <form onSubmit={submitHandler}>
      <div className="new-expense__controls">
        <div className="new-expense__control">
          <label>Title</label>
          <input type="text"
          value={userInput.title}
          onChange={titleChangeHandler} />
        </div>
        <div className="new-expense__control">
          <label>Price</label>
          <input
            type="number"
            min="100"
            step="100"
            value={userInput.price}
            onChange={priceChangeHandler}
          />
        </div>
        <div className="new-expense__control">
          <label>Date</label>
          <input
            type="date"
            min="2019-01-01"
            max={getTodayDate()}
            value={userInput.date}
            onChange={dateChangeHandler}
          />
        </div>
      </div>
      <div className="new-expense__actions">
        <button type="submit">Add Expense</button>
      </div>
    </form>
  );
};

export default ExpenseForm;

실습

  • 체크 버튼을 누르면 버튼 색이 바뀌게함
    -> 버튼 클릭했을 때 true, false로 나오게함
    -> className을 조건부로 입력
import React, { useState } from "react";
import "./CheckBoxStyle.css";

const CheckBoxStyle = () => {
  // 체크 상태를 관리
  const [isChecked, setIsChecked] = useState(false);

  // 클릭했을 때 발생하는 이벤트 핸들러
  const checkChangeHandler = (e) => {
    setIsChecked(!isChecked);
  };

  return (
    <div className="checkbox-container">
      <input
      type="checkbox"
      id="styled-checkbox"
      onChange={checkChangeHandler}
      />
      <label
        htmlFor="styled-checkbox"
        className={isChecked ? 'checked' : 'unchecked'}
        >
        Check me!
      </label>
    </div>
  );
};

export default CheckBoxStyle;

Props Drilling

  • react는 props를 통해 하향식 데이터 전달 가능
  • 자식컴포넌트 -> 부모컴포넌트 데이터 전달하는 법은 Props Drilling (props의 반대, 상향식 전달)
    -> 부모가 자식에게 함수를 props로 내려보내고 자식은 그 함수에 데이터를 담아서 위로 전달
  • 부모에서부터 자식 순서대로 타고타고 내려가서 전달해야 한다, 타겟 자식에서 데이터를 획득하면 함수의 파라미터로 보내고, 부모측에서 같은 이름으로 받아서 사용 가능

동적 리스트 렌더링

  • 배열에 데이터를 추가하면 화면에 자동으로 렌더링되게함
import React from 'react'
import ExpenseItem from './ExpenseItem'
import ExpenseFilter from './ExpenseFilter'

const ExpenseList = ({ expenses }) => {

  const onFilterChange = (filteredYear) => {
    // ExpenseFilter에 있는 선택된 연도값을 여기서 출력!
    console.log(filteredYear);
  }

  return (
    <div className='expenses'>
      <ExpenseFilter onChangeFilter={onFilterChange}/>
      { expenses
          .map(ex => <ExpenseItem title={ex.title} price={ex.price} date={ex.date} />) 
      }

    </div>
  )
}

export default ExpenseList

동적 리스트 렌더링 (필터 추가)

  • 여러개의 컴포넌트를 반복문으로 생성할 경우, 각 컴포넌트를 구분하기 위한
    속성으로 랜덤값(key)을 부여해야 한다.
    보통 DB에 있는 데이터를 사용하기에, PK를 쓴다.
    사용하지 않을 시에 올바르지 않은 렌더링이 됨.
import React, { useState } from 'react';
import ExpenseItem from './ExpenseItem'
import './ExpenseList.css'
import ExpenseFilter from './ExpenseFilter'

const ExpenseList = ({expenses}) => {

    // 선택된 연도로 재 렌더링하기 위해 연도를 상태값으로 관리
    const [filteredYear, setFilteredYear]
        = useState(new Date().getFullYear().toString()) // 올해 년도를 기본값으로.

    const onFilterChange = (value) => {
        console.log(value) // ExpenseFilter에서 올라온 데이터 (년도)
        setFilteredYear(value)
    };

    return (
        <div className="expenses">

            <ExpenseFilter onFilter={onFilterChange}/>
            { expenses
                .filter(ex => ex.date.getFullYear().toString() === filteredYear)
                .map(ex =>
                <ExpenseItem
                    // key는 여러개의 컴포넌트를 구분하기 위한 랜덤값. db에 있는 pk를 씀
                    // 여러개의 컴포넌트를 렌더링할땐 key를 써야한다.
                    key={Math.random().toString()} 
                    title={ex.title}
                    price={ex.price}
                    date={ex.date}
                />)}

        </div>
    );
};

export default ExpenseList;
profile
백엔드 개발자
post-custom-banner

0개의 댓글