[학습 기록] Day62: 리액트로 가계부 만들기

완성본 github 주소: https://github.com/yeom-kenco/account-book.git
완성본 배포 사이트: https://yeom-kenco.github.io/account-book/
💭가계부 이름에 내 닉네임만 뜨지만... input으로 받아서 뜰 수 있게했으면 좋았겠다고 뒤늦게 생각이 드는...

💭모달 구현 진행 예정....

💭유효성 검사 및 이에 대한 토스트창 띄우기 진행 예정...

 

과제 요구사항

예시

더보기

개요

수입과 지출을 기록하고 잔액을 관리할 수 있는 웹 애플리케이션으로, 모든 데이터는 브라우저의 로컬 스토리지에 저장된다.

React의 기본 개념, 상태 관리, 로컬 저장소 활용 능력을 평가하기 위한 것이다.

디자인의 정확한 구현보다 기능적 요구사항을 충족하는 데 중점을 둘 것.

기술 스택

  • React
  • CSS (또는 bootstrap, Tailwind CSS)
  • LocalStorage API

핵심 기능 요구사항

1. 잔액 표시

  • 현재 잔액을 상단에 표시
  • 수입 금액 합계 표시
  • 지출 금액 합계 표시

2. 거래 입력 기능

  • 텍스트 입력 필드: 거래 내역 설명 입력
  • 금액 입력 필드: 거래 금액 입력
  • 거래 유형 선택: 수입/지출 구분
  • '거래 추가' 버튼: 새 거래를 등록

3. 거래 내역 표시

  • 각 거래 항목에 대해 다음 정보 표시:
    • 금액 (수입은 녹색 '+', 지출은 빨간색 '-'로 표시)
  • 스크롤 가능한 거래 내역 목록
  • 거래 삭제 기능 (정말 삭제? 확인 기능 modal)

4. 데이터 저장

  • 모든 거래 데이터는 LocalStorage에 저장
  • 페이지 새로고침 후에도 데이터 유지

선택적 추가 기능

  • 예쁜 디자인 ^^
  • 카테고리 (예: "용돈", "영화", "외식" 기본 카테고리와 카테고리 추가)
  • 거래 편집 기능

데이터 구조 예시

const transactions = [
  {
    id: 1,
    description: "용돈",
    amount: 300000,
    type: "income",// "income" 또는 "expense"
    date: "2025-04-25"
  },
  {
    id: 2,
    description: "영화 관람",
    amount: 11000,
    type: "expense",
    date: "2025-04-25"
  }
];

 

UI 요구사항

  • 제공된 디자인 목업과 일치하지 않아도 됨
  • 반응형 디자인 구현 (모바일, 태블릿, 데스크톱)
  • 사용자 친화적인 인터페이스

개발 가이드라인

  1. 컴포넌트 기반 구조 사용 (최소 4개 이상의 컴포넌트로 분리)
  2. React Hooks 활용 (useState, useEffect 등)
  3. LocalStorage를 위한 커스텀 훅 작성
  4. 적절한 에러 처리 구현

 

 

과제 진행 과정 및 배운점 정리✍️ 

 

파일구조 및 개발 개요

account-book
├─ eslint.config.js
├─ index.html
├─ package-lock.json
├─ package.json
├─ public
├─ README.md
├─ src
│  ├─ App.css
│  ├─ App.jsx
│  ├─ assets
│  ├─ components
│  │  ├─ History.jsx
│  │  ├─ HistoryDetail.jsx
│  │  ├─ Summary.jsx
│  │  └─ TransactionUpdate.jsx
│  ├─ css
│  │  ├─ History.module.css
│  │  ├─ HistoryDetail.module.css
│  │  ├─ Summary.module.css
│  │  └─ TransactionUpdate.module.css
│  ├─ hooks
│  │  └─ useTransactions.js
│  ├─ index.css
│  └─ main.jsx
└─ vite.config.js

  • 총 4개의 컴포넌트로 분리해서 작업을 진행
  • 각 컴포넌트에서 state로 local Storage에 저장된 데이터의 상태를 공유할 수 있도록 useTransaction 훅 제작
    (hook을 통해 state를 부모에서 자식 컴포넌트로 props를 전달 받으면서 사용해야 같은 데이터를 공유하는 모든 컴포넌트가 따로 새로고침 없이 부모 컴포넌트가 리렌더링 됨과 동시에 함께 리렌더링된다.)
  • module방식의 css를 사용해, 각 컴포넌트에서 같은이름의 클래스를 사용해도 공유되지 않도록 함

useTransactions 훅 제작 : localStorage 기반의 거래 내역 상태 관리

이 훅은 거래 내역을 관리하며 localStorage를 통해 영속적으로 데이터를 저장한다.

 

useTransactions 전체코드

더보기
// src/hooks/useTransactions.js
import { useState, useEffect } from "react";

const LOCAL_STORAGE_KEY = "transactions";

const useTransactions = () => {
  const [transactions, setTransactions] = useState([]);

  // localStorage에서 초기 데이터 불러오기
  useEffect(() => {
    const storedData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
    if (storedData) {
      setTransactions(storedData);
    }
  }, []);

  // 거래 추가 함수
  const addTransaction = (newTransaction) => {
    const updatedTransactions = [newTransaction, ...transactions];
    setTransactions(updatedTransactions);
    localStorage.setItem(
      LOCAL_STORAGE_KEY,
      JSON.stringify(updatedTransactions)
    );
  };

  // 거래 삭제 함수
  const removeTransaction = (id) => {
    const updatedTransactions = transactions.filter((tx) => tx.id !== id);
    setTransactions(updatedTransactions);
    localStorage.setItem(
      LOCAL_STORAGE_KEY,
      JSON.stringify(updatedTransactions)
    );
  };

  return {
    transactions,
    addTransaction,
    removeTransaction,
  };
};

export default useTransactions;

 

 

1. 기본 구조

import { useState, useEffect } from "react";

const LOCAL_STORAGE_KEY = "transactions";

LOCAL_STORAGE_KEY는 localStorage의 키 이름이다.

거래 데이터를 문자열로 저장할 때 이 키를 기준으로 set/get을 수행하게 된다.

 

2. 초기 상태 설정

const [transactions, setTransactions] = useState([]);

거래 내역을 담는 state이다. 기본값은 빈 배열이다. 이 state가 변경되면 이 훅을 사용하는 컴포넌트들이 리렌더링 된다.

React의 핵심은 state 변화에 따른 UI 자동 갱신에 있기 때문이다.

 

3. localStorage에서 초기 데이터 불러오기

useEffect(() => {
  const storedData = JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY));
  if (storedData) {
    setTransactions(storedData);
  }
}, []);

 

의존성에 빈 배열을 넣은 useEffect는 컴포넌트가 처음 마운트될 때 한 번만 실행된다.
이 시점에서 localStorage에 저장된 거래 내역이 있는지 확인하고, 있다면 그것을 state에 반영한다.
즉, 사용자가 이전에 입력했던 데이터가 있다면 그것이 복구된다.
그렇기 때문에 새로고침해도 데이터가 유지되는 것이다.

 

4. 거래 추가 함수: addTransaction()

const addTransaction = (newTransaction) => {
  const updatedTransactions = [newTransaction, ...transactions];
  setTransactions(updatedTransactions);
  localStorage.setItem(
    LOCAL_STORAGE_KEY,
    JSON.stringify(updatedTransactions)
  );
};

거래를 추가하는 함수다. 새 거래는 가장 앞에 추가된다.
배열의 불변성을 유지하며 새로운 배열을 만들어 setTransactions에 넘긴다.
그 후 localStorage에도 업데이트된 배열을 문자열로 저장한다.
이때 중요한 것은 setTransactions이 호출되었다는 점이다.
이로 인해 이 훅을 사용하는 컴포넌트는 모두 리렌더링된다.

 

5. 거래 삭제 함수

const removeTransaction = (id) => {
  const updatedTransactions = transactions.filter((tx) => tx.id !== id);
  setTransactions(updatedTransactions);
  localStorage.setItem(
    LOCAL_STORAGE_KEY,
    JSON.stringify(updatedTransactions)
  );
};

삭제도 마찬가지이다. 해당 id를 제외한 배열을 만들어 setTransactions에 넣는다.
그리고 localStorage도 동기화한다.
이 역시 state가 변화했기 때문에 관련 컴포넌트는 즉시 업데이트된다.

 

6. 반환 객체

return {
  transactions,
  addTransaction,
  removeTransaction,
};

이 훅은 단순히 데이터를 반환하는 것이 아니라,
상태와 그 상태를 변화시키는 로직을 함께 리턴한다.
그렇기 때문에 이 훅을 사용하는 곳에서는 거래 내역을 조회하고 추가하거나 삭제할 수 있다.
이 모든 동작은 하나의 상태로 통합 관리되며, 이로 인해 앱의 일관성이 유지된다.


TransactionUpdate.jsx 컴포넌트 제작 : 사용자 input 관리 및 addTransaction() 함수 사용

TransactionUpdate 컴포넌트는 사용자로부터 거래 내역(내용, 금액, 수입/지출 여부)을 입력받아 새로운 거래 항목을 등록하는 UI이다. 

 

1. 상태(state) 선언

const [transactionDetailText, setTransactionDetailText] = useState("");
const [transactionDetailMoney, setTransactionDetailMoney] = useState("");
const [isIncome, setIsIncome] = useState(true);

이 컴포넌트는 총 3개의 상태를 가진다.

  • transactionDetailText: 거래 내용 입력값을 저장한다. 문자열 형태이다.
  • transactionDetailMoney: 금액 입력값을 저장한다. 초기값은 문자열이지만 숫자로 변환하여 처리한다.
  • isIncome: 수입인지 지출인지를 구분하는 불리언 값이다. true면 수입, false면 지출이다.

 

2. 입력값 핸들링

<input
  type="text"
  value={transactionDetailText}
  onChange={(e) => setTransactionDetailText(e.target.value)}
/>

 

input 요소의 값은 해당하는 state에 의해 결정되며, 값이 바뀌면 onChange 이벤트를 통해 다시 state를 업데이트하는 흐름이다.

= 화면(input)은 state를 읽기만 하고, 사용자가 입력하면 state를 업데이트만 한다.

React의 controlled component (제어 컴포넌트) 구조를 말한다.

Controlled Component란?
input의 value를 직접 관리하는 컴포넌트를 말한다.
즉, 화면에 보이는 input 값이 항상 state에 의해 결정된다는 것

 

이러한 구조의 장점은 다음과 같다:

  • 입력값을 완전히 제어할 수 있다.
  • 유효성 검사(validation)를 자유롭게 수행할 수 있다.
  • 폼 초기화 및 자동 포커스 등 부가 로직을 쉽게 붙일 수 있다.
더보기

1. 입력값을 완전히 제어할 수 있다.

  • 사용자가 입력하는 걸 실시간으로 감시할 수 있다.
  • 원하는 대로 값을 막거나, 자동으로 변형하거나, 아무 조작도 가능하다.
if (e.target.value.length > 10) {
  alert("10글자 넘으면 안돼요!");
  return;
}
setText(e.target.value);

=> 사용자가 10글자 넘게 입력하려고 하면 막아버릴 수 있다.

 

2. 유효성 검사(validation)을 자유롭게 수행할 수 있다.

  • 입력값이 숫자인지, 이메일 형식인지, 조건에 맞는지 등을 입력 즉시 확인할 수 있다.
if (!e.target.value.includes("@")) {
  setError("이메일 형식이 아닙니다!");
}

=> 입력값이 이상할 때, 바로 에러 메시지를 보여줄 수 있다.

 

3. 폼 초기화 및 자동 포커스 등 부가 로직을 쉽게 붙일 수 있다.

  • 폼 전송이 끝나면, input 값을 한 번에 리셋하거나
  • 특정 input에 자동으로 focus를 줄 수도 있다.

예시 (폼 초기화):

const handleSubmit = (e) => {
  e.preventDefault();
  // 데이터 서버로 전송
  setText(""); // 입력창 초기화
};

예시 (자동 포커스):

const inputRef = useRef();
useEffect(() => {
  inputRef.current.focus();
}, []);

return <input ref={inputRef} />;

=> 새로 렌더링되었을 때, 자동으로 커서를 깜빡이게 할 수 있다.

 

🔥 반대로 "Uncontrolled Component"는?

  • value를 직접 관리하지 않고, 브라우저가 알아서 관리한다.
  • 필요한 순간에만 ref를 통해 값을 가져온다.

이렇게 하면 편하긴 한데, 복잡한 폼 관리나 validation이 어렵다.

지출/수입 선택은 라디오 버튼으로 처리된다:

<input
  type="radio"
  name="isIncome"
  value="income"
  checked={isIncome === true}
  onChange={handleRadioChange}
/>

핸들러는 아래와 같다:

const handleRadioChange = (e) => {
  setIsIncome(e.target.value === "income");
};

여기서 "income" 문자열과 비교하여 isIncome을 true 또는 false로 설정한다.
React에서 라디오 버튼을 제대로 제어하려면 checked 속성을 활용한 이 방식이 필수적이다.

 

3. ➕ 거래 추가 함수

const handleAddTransaction = () => {
  const newTransaction = {
    id: Date.now(),
    description: transactionDetailText,
    amount: parseInt(transactionDetailMoney),
    type: isIncome ? "income" : "expense",
    date: new Date().toISOString().slice(0, 10),
  };

  addTransaction(newTransaction);

  setTransactionDetailText("");
  setTransactionDetailMoney("");
  setIsIncome(true);
};

거래 추가 버튼을 누르면 실행되는 함수다. 주요 흐름은 다음과 같다:

 

🔹 새로운 거래 객체 생성

  • 고유 id를 Date.now()로 생성
  • 내용, 금액, 유형(type), 날짜를 입력값에서 추출하여 구성

🔹 상위 함수에 전달

  • addTransaction() 함수는 props로 전달된 함수이며,
    최종적으로 useTransactions 훅의 addTransaction()을 호출하게 된다.
  • 이 흐름을 통해 최상위 상태가 변경되므로, 연결된 컴포넌트들은 자동 리렌더링 된다.

🔹 입력값 초기화

  • 입력창을 다시 빈 값으로 만들기 위해 상태를 초기화한다.
  • 이는 사용자 UX 측면에서 매우 중요하다.
    방금 작성한 값이 남아있으면 실수로 중복 입력할 수 있기 때문이다.

History 컴포넌트와 HistoryDetail 컴포넌트 정리

이 프로젝트에서는 거래 내역을 화면에 보여주는 역할을 History와 HistoryDetail이라는 두 컴포넌트가 맡고 있다.
각각 어떤 책임을 가지고 있는지, 그리고 구현 방식은 어떤 이점을 가지는지 하나씩 살펴보자.

 

1. History.jsx

 

1-1. 역할

History 컴포넌트는 전체 거래 내역을 관리한다.
구체적으로는 다음과 같다:

  • 거래 목록(transactions)을 받아온다.
  • 각각의 거래를 HistoryDetail 컴포넌트로 넘겨서 개별 렌더링한다.
  • 거래가 아예 없다면 "거래 내역이 없습니다"라는 안내 문구를 보여준다.

즉, "거래 목록 전체"를 책임지는 상위 컴포넌트라 할 수 있다.

 

1-2. 주요 구조

{transactions.length === 0 ? (
  <p>거래 내역이 없습니다🤔</p>
) : (
  transactions.map((t) => (
    <HistoryDetail key={t.id} transaction={t} onDelete={removeTransaction} />
  ))
)}

여기서 중요한 흐름은 다음과 같다:

  • 거래 목록이 비었는지(length === 0) 먼저 체크한다.
  • 비었다면 안내 문구를 보여주고,
  • 그렇지 않으면 .map()으로 거래 하나하나를 HistoryDetail 컴포넌트로 넘긴다.

2. HistoryDetail.jsx

 

2-1. 역할

HistoryDetail 컴포넌트는 "거래 하나"를 화면에 표시하는 역할을 한다.
그리고, 마우스를 올렸을 때 삭제 버튼이 나타나고, 클릭 시 삭제 요청을 보내는 로직을 담당한다.

즉, 개별 거래 단위를 다루는 세부 컴포넌트라 할 수 있다.

 

2-2. 주요 로직

① hover 상태 관리

const [hover, setHover] = useState(false);
  • 마우스를 올리거나(onMouseEnter)
  • 마우스를 내릴 때(onMouseLeave)

hover 상태를 바꾼다.
그렇기 때문에 사용자가 거래 항목에 마우스를 올렸을 때만 삭제 버튼(❌)이 나타난다.

 

② 삭제 버튼 동작

const handleDelete = () => {
  const confirm = window.confirm("정말 삭제하시겠습니까?");
  if (confirm) onDelete(transaction.id);
};
  • 삭제 버튼을 클릭하면 window.confirm()으로 사용자에게 한 번 묻는다.
  • "확인"을 누르면 onDelete(transaction.id)가 호출된다.

여기서 onDelete는 History 컴포넌트에서 removeTransaction을 넘겨준 함수다.

 

③ 거래 내용 표시

<div className={css.text}>{transaction.description}</div>
{transaction.type === "income" ? <span>+</span> : <span>-</span>}
<div className={css.amount}>{transaction.amount.toLocaleString()}원</div>
<div className={css.date}>{transaction.date}</div>
  • 거래 설명(description), 금액(amount), 날짜(date)를 각각 따로 나눠서 표시한다.
  • transaction.type이 "income"이면 금액 앞에 + 표시를 붙이고, "expense"면 -를 붙인다.
  • toLocaleString()을 써서 금액을 보기 좋게 1,000단위로 콤마(,)를 넣는다.

3. CSS 추가 설명 : 거래 항목 삭제 버튼 구현 ⭐⭐⭐

이 프로젝트에서는 거래 항목마다 마우스를 올렸을 때(hover) 삭제 버튼(❌)이 부드럽게 나타나도록 만들었다.
하지만 이 기능을 구현하는 데에는 신경 써야 할 부분이 꽤 많았다.

닫기 버튼을 자연스럽고 깔끔하게 띄우기 위해서는,
position 설정과 opacity 전환, 그리고 상대적인 위치 계산을 제대로 다뤄야 한다.

1. 버튼을 아이템 바깥에 띄워야 했다

1-1. 문제

보통 버튼은 아이템 안쪽에 만들어지는데, 이 경우에는
아이템 왼쪽 바깥으로 ❌ 버튼을 살짝 튀어나오게 배치해야 했다.

그렇기 때문에 기본적인 배치 방법으로는 부족했다.

1-2. 해결 방법

.deleteButton {
  position: absolute;
  left: -28px; /* 아이템 왼쪽 바깥으로 밀어낸다 */
  top: 50%;
  transform: translateY(-50%);
}
  • position: absolute;
    → 버튼을 .item 기준으로 절대 위치에 배치할 수 있게 만든다.
  • left: -28px;
    왼쪽으로 28px 밀어서 아이템 바깥으로 나가게 만든다.
  • top: 50%; transform: translateY(-50%);
    → 버튼을 세로 가운데 정렬시킨다. → top: 50%로 위에서 절반만큼 내려온 다음, translateY(-50%)로 자기 키(height)의 절반만큼 다시 올려서 정확히 중앙에 맞춘다.

✅ 이 세 가지 설정이 맞아야, 버튼이 예쁘게 좌측에 "살짝 튀어나온" 느낌으로 고정된다.

 

2. 버튼이 처음에는 안 보여야 했다

항상 버튼이 보인다면 화면이 너무 복잡하고 지저분해진다.
마우스를 올렸을 때만 자연스럽게 나타나야 했다.

그래서 처음에는 opacity: 0으로 투명하게 숨기고,
hover 되었을 때만 opacity: 1로 부드럽게 나타나게 만들었다.

.deleteButton {
  opacity: 0;
  transition: opacity 0.2s ease;
}

.item:hover .deleteButton {
  opacity: 1;
}
  • 기본 상태에서는 opacity: 0 → 완전히 투명.
  • .item에 hover가 발생하면, .deleteButton의 opacity: 1로 전환.
  • transition: opacity 0.2s ease;를 걸어줬기 때문에,
    → 단번에 튀어나오는 것이 아니라, 0.2초 동안 부드럽게 나타난다.

3. 그 외 설정들

 

버튼 클릭 가능 영역

padding: 2px 6px;
cursor: pointer;
  • 클릭하기 편하도록 약간의 padding을 줬다.
  • cursor: pointer;로 마우스가 올라갔을 때 손가락 모양으로 바뀌게 했다.

버튼 시각적 구분

border-radius: 3px;
font-size: 12px;
z-index: 2;
  • z-index: 2로 레이어를 살짝 위로 띄워서, 다른 요소보다 항상 위에 보이게 했다.

 

닫기 버튼 하나 구현하는 데에도 생각보다 많은 CSS 테크닉이 들어갔다.

(1) 위치를 절대좌표로 띄우고 → (2) opacity로 등장 애니메이션을 주고 → (3) 클릭 편의성과 디자인 세부 튜닝을 더했다.

 

📌 이런 걸 기억해두자!

  • hover 효과를 버튼이 아니라 부모 요소에 걸자
    (그래야 버튼이 아닌 곳에 hover 해도 부드럽게 나타난다)
  • left, top, transform으로 정확한 위치 잡기
    (translateY로 세로 중앙 맞추는 건 CSS에서 자주 쓰이는 고급 팁이다)
  • opacity + transition 조합은 매우 강력하다
    (애니메이션 만들 때 필수 테크닉!)

 

미비한 점

💡 현재 내 방식 요약

  • transactions (거래내역)라는 state를 부모 컴포넌트에서 만들고,
  • 이걸 props로 자식 컴포넌트들에 전달했다.
  • 새로운 거래를 추가하거나 삭제할 때, 부모의 state를 업데이트해주고,
  • 그 덕분에 자식들도 리렌더링되도록 연결되어 있다.

💬 그런데 강사님 피드백은?

"props로 다 전달하는 대신, 필요한 컴포넌트가 스스로 데이터를 가져오게 하면 어때?"

지금 방식 추천하는 방식
부모가 모든 state 관리 각 컴포넌트가 필요한 데이터/함수만 직접 가져옴
props로 모든 데이터를 넘김 필요할 때 getTransactions(), saveTransaction() 같은 함수 호출
트랜잭션 추가/삭제: 부모 → 자식 트랜잭션 추가/삭제: 자식 스스로

 

📚 왜 이런 방식이 더 좋을까?

  1. 컴포넌트 독립성 증가
    • 각 컴포넌트가 필요한 데이터만 직접 가져오기 때문에 의존성(props) 이 줄어듦.
    • 유연하고 재사용성 높은 컴포넌트를 만들 수 있다.
  2. props 관리 지옥 방지
    • 컴포넌트가 많아지면 props로 주고받는 게 복잡해진다.
    • "props drilling" (prop을 자식 → 손자 → 증손자까지 넘기는 문제)을 예방할 수 있다.
  3. 수정/유지보수 편함
    • 나중에 구조가 커져도 "부모 수정 → 자식 수정"의 연쇄작업이 적어진다.
  4. 코드가 깔끔하고 가독성 좋아짐
    • 필요한 컴포넌트에서 필요한 데이터/함수만 딱 불러서 쓰면 된다.

 

🔥 구체적으로 어떻게 개선하는 걸까?

 

1. useTransactions.js 훅에서 useState랑 useEffect를 빼버리기: 더이상 상태를 여기서 만들지 않는다.

2. 대신 localStorage를 직접 다루는 순수 함수들만 만들기

3. 컴포넌트 안에서 직접 가져다 쓴다.

예를 들어, History 컴포넌트 안에서는:

import { getTransactions, deleteTransaction } from "../utils/localStorageUtils";
import { useState, useEffect } from "react";

const History = () => {
  const [transactions, setTransactions] = useState([]);

  useEffect(() => {
    const data = getTransactions();
    setTransactions(data);
  }, []);

  const removeTransaction = (id) => {
    deleteTransaction(id);
    setTransactions(getTransactions()); // 삭제 후 다시 불러오기
  };

  // 나머지 렌더링
};

→ 이제는 부모가 모든 state를 가지지 않아도 되고,
→ 각 컴포넌트가 필요할 때 알아서 localStorage를 읽어와서 데이터를 갱신하는 것

 

4. useEffect() 사용

  • 왜 useEffect를 써야 하냐면?
  • localStorage는 "브라우저 저장소"라서, 실제 반영된 데이터를 다시 읽어와야 리렌더링이 정확히 일어난다.
  • 특히, 저장/삭제 같은 작업을 한 뒤 localStorage를 다시 읽어서 state를 업데이트하는 게 중요함

 

 

회고

오늘은 팀프로젝트에 앞서, 팀을 짜기위해 개별 실력을 파악하신다고 실습 과제를 내주셨다.
사실 오전 2시간에는 블로그 꾸미기를 하다가 시간을 다 날려서(ㅎㅎ;) 점심먹고부터 열심히 하고있었는데,
강사님께서 왜 다들 아직도 안내냐고, 다들 너무 디테일하게 기능 구현하고, 너무 잘해서 그런거냐고 그런거죠? 다들 그러시느라 그런가보다~ 이러셔서...마음이 아팠다..^^;;

사실상 실력자 대략 3분의 1 정도 빼고는 절대 3시간 만에 gpt없이 요구사항 다 못만들지 않을까라는 생각이었는데, 강사님께서 되게 당황하시길래 '아 이정도는 다들 2-3시간 컷으로 디테일한 기능 구현까지 가능하겠지.'라고 예상하신 것 같았다....ㅜㅜ

결국 나는 gpt손 빌리기 작전^__^ 
커스텀 훅 만드는 것도 감이 아예 안왔고~... css 미세 조정 하다보니 그냥 시간이 사라졌다!! 막막 내 맘대로 적용 안되는 css들과 똑같은 input구조인데 한개에만 자꾸 css적용되고 난리~!!!! gpt와 함께 해버렸지만...배워간게 있으면 된거 아닐까요?ㅜㅜㅎㅎ

 

+) 1대1로 내가 만든 결과물에 대한 코드 피드백을 받아본 건 살면서 처음이었다! 그전까지는 이런 피드백 과정이 얼마나 중요하고 값진 시간인지 체감해볼 기회가 없었는데, 단번에 깨달을 수 있었다....ㅜㅜ특히나 나같은 초짜는 작동만 잘 되면 내가 뭔가 잘못된 구조로 개발하고있는지 오래도록 깊이 고민하지 않는 한 스스로 인지하기 어렵다...무엇보다 gpt의 경우 거짓정보를 진짜인 것 처럼 진실과 섞여서 교묘하게...늘 좋은 말만 해주기때문에 전적으로 신뢰하기 어려운 것 같다. 저번 프로젝트때도 전혀 잘못된 과정이었는데 "훌륭해!"라고 해주더니만 이번에도 내가 한 구조를 추천한다고 해줬었기 때문 ㅋㅅㅋ;; 

그래도 강사님께 피드백 받고 한 번에 무슨 말씀이신지 이해하기 어려웠는데, 다른분들 코드도 참고하고 gpt한테 질문하며 완전히 이해할 수 있었다. 어쨌든 따봉gpt야 고마어~