🎨 디자인 시안

초안으로 받은 디자인을 내 스타일에 맞춰 새롭게 리디자인 했다.

리디자인을 하면서 Header, Balance, Add, List 컴포넌트 구성도 나눠봤다.

전반적으로 디자인 작업을 먼저 진행한 뒤, 디자인이 완료되면 기능을 구현하는 방식으로 개발을 진행했다.
디자인을 구현할 때는 각 컴포넌트의 역할에 따라 border 값을 설정해, 작업 중인 영역을 시각적으로 구분하며 작업을 이어갔다.
🧩 주요 컴포넌트 및 코드 설명

1. components/ [컴포넌트 파일들이 들어 있는 폴더 (각 UI 요소를 나눠서 관리)]
- Add.jsx : 수입/지출 내역을 추가하는 입력 폼 컴포넌트
- Balance.jsx : 현재 잔액(balance) 을 보여주는 컴포넌트
- Header.jsx : 프로젝트 이름
- List.jsx : 등록한 거래 내역 리스트를 보여주는 컴포넌트
- 각각 .module.css 파일로 스타일도 컴포넌트 단위로 분리해서 관리 중
- → CSS Modules 방식은 클래스 이름 충돌을 막아줘서 유지보수에 좋다.
- 2. hooks/ [커스텀 훅(Custom Hook) 을 정의하는 폴더]
- useLocalStorage.js: localStorage를 쉽게 쓰기 위한 재사용 가능한 함수
- → 데이터를 저장하거나 불러올 때 사용한
- 3. src/ [실제 앱의 중심이 되는 코드들이 들어가는 폴더]
- App.jsx : 컴포넌트들을 불러와서 실제 앱 UI를 구성하는 핵심 파일
- main.jsx : 앱을 브라우저에 렌더링하는 최초 진입 파일
- index.css : 전역 CSS 스타일
* 코드 설명은 .jsx 파일 위주로 적겠다.
App.jsx

Header.jsx, Balance.jsx, Add.jsx, List.jsx파일의 기능별로 구현하기 전에
화면에 각 컴포넌트가 렌더링되게 구현했다.
import React, { useRef } from 'react';
import Balance from '../components/Balance';
import List from '../components/List';
import Add from '../components/Add';
import Header from '../components/Header';
// 나중에 설명
import useLocalStorage from '../hooks/useLocalStorage';
Header.jsx
Header.jsx에서는 프로젝트의 이름을 적어줬다.

비교적 간단하게 끝난 작업이다.
import React from 'react';
import css from './Header.module.css';
const header = () => {
return (
<div>
<h1 className={css.title}>WalletWatcher 💸</h1>
</div>
);
};
export default header;
jsx에서 css를 불러오는 방식이 두가지가 있다.
1. 글로벌 방식 (Global css)
import './Header.css'
...
<h1 className="title">
전체 애플리케이션에 영향을 미치는 방식
2. 모듈 css 방식 (css module)
import styles from './Header.module.css'
...
<h1 className={styles.title}>
해당 컴포넌트에만 국한된 클래스 이름으로 바뀐다.
카멜케이스를 이용해 className을 작성한다.
2번 모듈 css방식을 선택해 css를 불러왔다. (이후 설명할 jsx파일들에서도 동일한 방식을 사용했다.)
Balance.jsx를 렌더링하는 App.jsx 코드
const [moneyList, setMoneyList] = useLocalStorage('moneyList', []);
useLocalStorage를 제외하고 설명해보겠다. (밑에서 useLocalStorage에 대해 설명하겠다.)
useLocalStroage를 사용하기 전,
const [moneyList, setMoneyList] = useState([]);
moneyList는 단순한 변수를 저장하는게 아니다.
데이터 변경이 자주 일어나기때문에 setMoneyList로 상태를 업데이트를 해줘야 실시간으로 바뀐다.
사용자가 데이터를 추가하거나 삭제할 때 화면에 렌더링이 되어야함으로 렌더링에 영향을 주는 값인 상태(state)로 다뤘다.
- moneyList: 현재 수입/지출 항목들이 들어 있는 데이터 리스트 상태
- setMoneyList: 이 리스트를 추가하거나 삭제할 때 사용하는 함수
- 초깃값 []: 처음엔 비어있는 배열로 시작했다.
useState만 사용하면 페이지를 새로고침할 때 데이터가 모두 사라지게된다.
그래서 이 문제를 해결하려고 useLocalStorage라는 커스텀 훅을 만들어 브라우저의 localStorage와 연동하여 데이터를 유지했다.
return (
<>
...
<Balance moneyList={moneyList} />
...
</>
);
Balance 컴포넌트에 moneyList라는 이름의 props(속성)을 전달한다.
Balance.jsx
Balance.jsx는 수익, 소비 표시 및 수입과 지출 내역을 기반으로 잔액을 계산해 보여주는 컴포넌트이다.

import React from 'react';
import css from './Balance.module.css';
필요한 파일들을 import해준다.
// App.jsx
<Balance moneyList={moneyList} />
// Balance.jsx
const Balance = ({ moneyList }) => {...}
Balance 컴포넌트에서는 전달받은 moneyList prop을 구조 분해 할당을 통해 꺼내어 사용한다.
const income = moneyList
.filter((item) => item.type === 'income')
.reduce((sum, item) => sum + parseInt(item.amount), 0);
const expense = moneyList
.filter((item) => item.type === 'expense')
.reduce((sum, item) => sum + parseInt(item.amount), 0);
① .filter((item) => item.type === 'income')
- moneyList 배열에서 item.type이 income인 항목만 새로운 배열로 추출한다.
② .reduce((sum, item) => sum + parseInt(item.amount), 0);
- filter()로 추출한 수입 항목 배열에 대해, amount값을 더해준다.
- sum은 총 값이고, item.amout는 현재 금액이다.
- parseInt를 사용한 이유는 혹시 모를 문자열을 숫자로 바꾸는 작업을 하는 것이다.
- 0은 sum의 초기값을 나타낸다.
expense(소비)도 동일하게 처리된다.
const balance = income - expense;
잔액을 나타내기 위해 수익 - 소비 를 해줬다.
return (
<section className={css.balanceContainer}>
<div className={css.balanceAmount}>
<h3>잔액</h3>
<p>
<span className={css.amount}>{balance.toLocaleString()}</span>
<span className={css.unit}>원</span>
</p>
</div>
<div className={css.inExContainer}>
<div className={css.inExBox}>
<h2 className={css.inTitle}>수익</h2>
<p className={css.inPrice}>
<span>{income.toLocaleString()}</span>
<span className={css.won}>원</span>
</p>
</div>
<div className={css.inExBox}>
<h2 className={css.ExTitle}>소비</h2>
<p className={css.exPrice}>
<span>{expense.toLocaleString()}</span>
<span className={css.won}>원</span>
</p>
</div>
</div>
</section>
);
<span className={css.amount}>{balance.toLocaleString()}</span>
balance라는 변수를 jsx 내부에서 출력하기 위해 { }를 사용했다.
이때 toLocalString()을 이용해 숫자에 천 단위 쉼표(,)가 자동으로 포맷팅 되도록 했다.
포맷팅이란?
코드나 데이터의 "모양"을 보기 좋게 정리하는 것
이외의 변수들(income, expense)도 동일한 목적으로 사용했다.
🖐🏻여기서 잠깐! jsx에서 저장한 JavaScript변수를 호출할때 꼭 { }를 이용해야하나요?
→ JSX는 JavaScript와 HTML을 섞어 쓸 수 있는 문법이다.
→ JavaScript 표현식을 쓸 수 있도록 중괄호 {} 안에서만 JS 코드 실행을 허용한다.
Add.jsx를 렌더링하는 App.jsx 코드
const idRef = useRef(0);
const onCreate = (text, amount, type) => {
const newItem = {
id: idRef.current + 1,
text: text,
amount: Number(amount),
type,
checked: false,
};
console.log('새 항목 추가됨:', newItem);
setMoneyList([newItem, ...moneyList]);
idRef.current += 1;
};
const idRef = useRef(0);
idRef는 객체를 생성한다. 이후 idRef.current를 계속 변경하면서 컴포넌트는 리렌더링이 되지 않게 했다.
그렇다면 왜 useState를 안사용하고 useRef를 사용했을까?
- useState를 쓰면 값이 바뀔때마다 리렌더링이 된다.
- useRef는 .current값이 변경해도 리렌더링이 되지 않는다.
- id처럼 화면에 직접 표시되지 않는 값에 사용하면 조오타!
const onCreate = (text, amount, type) => {
const newItem = {
id: idRef.current + 1,
text: text,
amount: Number(amount),
type,
checked: false,
};
console.log('새 항목 추가됨:', newItem);
setMoneyList([newItem, ...moneyList]);
idRef.current += 1;
};
onCreate를 화살표 함수로 선언했다.
- 매개변수
- text: 사용자가 입력한 내용 (무엇을 샀는지)
- amount: 입력한 금액
- type: 수입인지 소비인지 (radio 버튼 선택 결과)
- 객체 생성
- 새로운 항목을 객체로 생성하고 변수 newItem에 저장
- 각 항목은 다음과 같은 속성을 가짐:
- id: 각 항목을 식별하기 위한 고유값 (useRef로 관리)
- text, amount, type: 사용자가 입력한 값
- checked: 체크박스 기능을 위해 기본값 false
- 상태 업데이트
- setMoneyList()를 통해 기존 moneyList 앞에 newItem을 추가함
- 즉시 반영되도록 리스트 상태가 갱신됨
1️⃣ 왜 newItem을 함수 바깥이 아닌 안에서 만들었을까? 🤔
함수 안에 newItem을 선언한 이유는 사용자가 새로운 항목을 추가할 때마다 매번 다른 애용으로 객체가 만들어져야하기 때문이다.
- text, amount, type 값이 그때의 입력값으로 채워진다.
- 사용자가 두 번째로 입력하면 새로운 값들로 다시 newItem이 만들어진다.
만약 newItem을 함수 바깥에 적으면 한 번 만들어진 객체가 계속 재사용돼서, 모든 입력이 마지막 값으로 덮어쓰기 될 수도 있다.
2️⃣ 왜 checked는 false로 했을까? 🤔
checked는 해당 항목이 체크박스에서 선택되었는지 여부를 나타내는 불리언(boolean) 값이다.
새로운 항목이 생성될 때는 사용자가 아직 아무것도 선택하지 않았기 때문에,
기본값으로 false를 설정한다.
즉, checked: false는 아직 선택되지 않은 초기 상태를 의미
return (
<>
...
<Add onCreate={onCreate} />
...
</>
);
Add 컴포넌트에 onCreate라는 이름의 props(속성)을 전달한다.
Add.jsx

import React, { useState, useRef } from 'react';
import css from './Add.module.css';
필요한 파일들을 import해준다.
const [text, setText] = useState('');
const [amount, setAmount] = useState('');
const [type, setType] = useState('');
const textRef = useRef();
① const [text, setText] = useState('');
사용자가 작성한 텍스트 입력값을 저장하는 state
- text는 현재 입력창에 있는 문자열
- setText는 입력값이 바뀔 때 업데이트하는 함수
- onChange={(e) => setText(e.target.value)} 이렇게 연결된다.
② const [amount, setAmount] = useState('')
사용자가 입력한 금액을 저장하는 state
- 나중에 저장할 때 Number(amount)로 숫자로 변환해준다.
- 역시 input과 연결해서 onChange={(e) => setAmount(e.target.value)}
③ const [type, setType] = useState('')
수익 or 소비 중 어떤 항목인지 선택 상태를 저장
- 라디오 버튼을 선택하면 이 값이 "income" 또는 "expense"로 바뀐다.
- checked={type === 'income'} 처럼 연결해서 현재 선택된 걸 표시해준다.
④ const textRef = useRef();
input에 포커스를 다시 줄 때 사용하는 도구
- 항목 추가 후에 textRef.current.focus()를 호출하면
- → 텍스트 입력창에 자동으로 커서가 이동함
- 즉, 입력 편의성 개선용
const handleSumbit = (e) => {
e.preventDefault();
if (!text || !amount) return;
onCreate(text, amount, type);
setText('');
setAmount('');
setType('');
textRef.current.focus();
};
이 코드는 form을 처리하는 함수이다.
✅ 전체 함수의 목적
const handleSumbit = (e) => {
...
}
이 함수는 폼이 제출(submit) 될 때 실행된다.
<form onSubmit={handleSubmit}> 형태로 연결돼 있어서, 사용자가 "추가" 버튼을 누르면 이 함수가 작동한다.
1️⃣ e.preventDefault();
e.preventDefault();
폼을 전송하면 기본적으로 페이지가 새로고침된다. 이걸 막기 위해 preventDefault() 쓴다.
즉, React에서 폼 데이터를 JS로 다룰 수 있게 해주는 필수 코드
2️⃣ if (!text || !amount) return;
if (!text || !amount) return;
입력 값이 비어있으면 전송하지 않도록 유효성 검사를 한다.
text 또는 amount 중 하나라도 없으면 → 그냥 함수 종료(return)
4️⃣ onCreate(text, amount, type);
onCreate(text, amount, type);
App.jsx에서 전달된 새 항목 추가 함수를 호출하는 부분이다.
현재 상태(state)에 저장된 값을 넘겨서 새로운 항목을 생성한다.
onCreate()를 불러와서 썼는데 import를 안했는데 어떻게 작동하는거지? 🤔
// App.jsx
<Add onCreate={onCreate} />
App.jsx에서 Add 컴포넌트에 onCreate라는 props를 전달한다.
그러면 Add.jsx에서는 받은 함수를 내부에서 호출가능!
그래서 import를 안해도 되는 이유는 onCreate는 App 컴포넌트 안에서 정의한 함수이기 때문에, 외부에서 import할 필요 없이, Add 컴포넌트에 props로 넘겨주면 된다.
// 부모(App.jsx)
function 부모() {
const 함수 = () => console.log("불렀다!");
return <자식 전달={함수} />;
}
// 자식(Add.jsx)
function 자식({ 전달 }) {
전달(); // 이렇게 불러도 됨
}
5️⃣ textRef.current.focus();
textRef.current.focus();
textRef는 <input ref={textRef}>에 연결된 요소, 이 코드로 인해 항목 추가 후 커서가 자동으로 제목 input 칸에 깜빡이게 된다.
사용자 편의성을 높이는 디테일한 UX 처리이당
<section className={css.addContainer}>
<h3>오늘 뭐 샀나요?</h3>
<form onSubmit={handleSumbit}>
<div className={css.inputField}>
<input
ref={textRef}
value={text}
onChange={(e) => setText(e.target.value)}
type="text"
placeholder="무엇에 돈을 썼나요?"
required
/>
<input
type="number"
placeholder="얼마였나요?"
value={amount}
onChange={(e) => setAmount(e.target.value)}
required
/>
</div>
<div className={css.addRow}>
<div className={css.checkboxContainer}>
<input
type="radio"
id="income"
name="type"
value="income"
checked={type === 'income'}
onChange={(e) => setType(e.target.value)}
/>
<label htmlFor="income">수익</label>
<input
type="radio"
id="expense"
name="type"
value="expense"
checked={type === 'expense'}
onChange={(e) => setType(e.target.value)}
/>
<label htmlFor="expense">소비</label>
</div>
<button className={css.addBtn} type="submit">
추가
</button>
</div>
</form>
</section>
e.target.value을 왜 사용했지? 🤔
(e) => setText(e.target.value)
사용자가 <input> 같은 입력창에 무언가를 입력할 때, 그 입력값(value) 을 가져오는 역할을 한다.
- e → event 객체 (이벤트에 대한 정보가 담겨 있음)
- e.target → 이벤트가 발생한 HTML 요소 (ex: 그 <input> 태그)
- e.target.value → 그 <input>에 들어 있는 값! (사용자가 입력한 글자)
<input type="text" onChange={(e) => console.log(e.target.value)} />
- 사용자가 "사과"를 입력하면 콘솔에 사과가 찍힌다. 🍎
- "바나나" 입력하면 콘솔에 바나나가 찍힌다. 🍌
그럼 왜 쓰지?😅
👉🏻 useState로 관리하는 상태를 실시간으로 바꾸기 위해 사용한다.
const [text, setText] = useState('');
<input value={text} onChange={(e) => setText(e.target.value)} />
이 코드를 쓰면 사용자가 타이핑할 때마다 text 값이 즉시 업데이트된다.
=> 양방향 바인딩 (입력창 ↔ 상태 변수가 연결됨)
List.jsx를 렌더링하는 App.jsx 코드
const onDelete = (targetId) => {
const isConfirmed = window.confirm('정말로 삭제하시겠어요?');
if (isConfirmed) {
setMoneyList(moneyList.filter((item) => item.id !== targetId));
}
};
targetId란?
- 삭제하고 싶은 항목의 id를 의미
- 예: 어떤 버튼을 눌렀을 때, 그 항목의 id가 4라면 onDelete(4) 호출!
const isConfirmed = window.confirm('정말로 삭제하시겠어요?');
- 브라우저 팝업창을 띄워서 확인을 받는 함수야!
- 사용자가 '확인' 누르면 true , '취소' 누르면 false
setMoneyList(...)
- 필터링된 리스트를 다시 moneyList 상태로 업데이트해줘
moneyList.filter((item) => item.id !== targetId)
- moneyList 배열에서 id가 targetId인 항목 제외하고 남은 애들만 필터링한다.
- 즉, 삭제할 항목 빼고 나머지만 남겨준다.
🔍 filter()에 대해 좀 더 알아보자!
filter는 말이죠~ 🏇🏻
const newArray = oldArray.filter((item) => 조건);
- oldArray의 각 요소를 item이라고 하고
- 그 item에 대해 조건이 true인 것들만 newArray에 담아줘
setMoneyList(moneyList.filter((item) => item.id !== targetId));
- 여기서 moneyList는 거래 내역 배열이야.
- item은 그 안에 들어 있는 각 객체 하나씩!
- item.id !== targetId → 삭제하려는 id가 아닌 것만 남기겠다는 뜻
const moneyList = [
{ id: 1, text: '아이스 아메리카노', amount: 4000 },
{ id: 2, text: '알바비', amount: 400000 },
{ id: 3, text: '케이크', amount: 4000 }, ];
onDelete(2) 실행 시
moneyList.filter((item) => item.id !== 2)
[
{ id: 1, text: '커피', amount: 3000 },
{ id: 3, text: '케이크', amount: 4000 },
]
즉, id가 2인 알바비는 없어지고 나머지만 남는다.
return (
<>
...
<List moneyList={moneyList} onDelete={onDelete} />
</>
);
Add 컴포넌트에 moneyList, onDelete라는 이름으로 props(속성)을 전달한다.
List.jsx

import css from './List.module.css';
const List = ({ moneyList, onDelete }) => {
return (
<section className={css.listContainer}>
<h3 className={css.listHeader}>거래 내역</h3>
{moneyList.map((item) => (
<div className={css.listItem} key={item.id}>
<div className={css.itemLeft}>
<div className={css.itemText}>{item.text}</div>
</div>
<div className={css.itemRight}>
<div
className={`${css.price} ${
item.type === 'income' ? css.income : css.expense
} `}
>
<span className={css.sign}>
{item.type === 'income' ? '+' : '-'}
</span>
<span className={css.amount}>
{Number(item.amount).toLocaleString()}
</span>
<span className={css.won}>원</span>
</div>
<button className={css.deleteBtn} onClick={() => onDelete(item.id)}>
🗑️
</button>
</div>
</div>
))}
</section>
);
};
export default List;
1. 삭제 버튼 클릭 이벤트 🗑️
<button className={css.deleteBtn} onClick={() => onDelete(item.id)}>
- onClick={() => onDelete(item.id)}
- → 이건 버튼을 클릭했을 때 onDelete() 함수가 실행되도록 설정한 거야. → 현재 item.id를 onDelete()에 전달해서 해당 항목을 삭제하게 된다.
- onDelete는 App.jsx에서 props로 전달해줬다.
<List moneyList={moneyList} onDelete={onDelete} />
👉 즉, 이건 삭제 버튼 누르면 해당 아이템의 ID로 삭제 요청을 보낸다는 의미
2. 조건부 클래스 적용 (스타일 변경)
className={`${css.price} ${
item.type === 'income' ? css.income : css.expense
}`}
- ${} 안에서 item.type이 "income"인지 "expense"인지에 따라 다른 색상의 클래스를 적용한다.
- 수익이면 css.income (초록색)
- 소비면 css.expense (빨간색)
3. 수입/지출 기호 구분
{item.type === 'income' ? '+' : '-'}
수익이면 +, 아니면 -를 앞에 붙여서 보여준다.
useLocalStorage
localStorage에 데이터를 저장하고, 꺼내 쓰면서도 useState처럼 상태 관리를 함께 할 수 있게 도와주는 커스텀 훅이다.
useLocalStorage를 App.jsx에서 사용하는 방법
const [moneyList, setMoneyList] = useLocalStorage('moneyList', []);
- 'moneyList': localStorage에 저장될 키 이름
- []: 초기값
- moneyList: 현재 저장된 값
- setMoneyList(): 값 업데이트 (이때 localStorage에도 자동 저장)
👉 상태값을 useState로 관리하되, 로컬스토리지에도 자동으로 저장되게 하려는 것!
useLocalStorage.js
왜 useLocalStorage는 .jsx로 저장하지 않을까? 🤔
useLocalStorage.js는 컴포넌트가 아닌 함수(훅)이기 때문이다.
확장자 | 주 용도 | 특징 |
.jsx | React 컴포넌트 | JSX 문법(HTML처럼 생긴 코드)을 포함 |
.js | 일반 함수, 유틸, 훅 등 | 순수 JavaScript 코드 작성용 |
1) 상태 초기값을 로컬스토리지에서 가져오기
const [storedValue, setStoredValue] = useState(() => {
try {
const item = localStorage.getItem(key); // ① 로컬스토리지에서 불러오기
return item ? JSON.parse(item) : initialValue; // ② 있으면 파싱해서 반환, 없으면 초기값
} catch (err) {
console.error('LocalStorage 읽기 실패:', err);
return initialValue;
}
});
- 처음 컴포넌트가 렌더링될 때 localStorage.getItem(key)를 사용해서 저장된 값을 꺼낸다.
- 없으면 initialValue를 기본값으로 사용
- JSON.parse()는 저장된 문자열을 다시 JS 객체로 바꾸는 역할
2) 값이 바뀔 때마다 로컬스토리지에 저장
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(storedValue));
} catch (err) {
console.error('LocalStorage 저장 실패:', err);
}
}, [storedValue, key]);
- storedValue가 바뀔 때마다 자동으로 localStorage에 반영된다.
- JSON.stringify()는 객체나 배열을 문자열로 바꿔서 저장할 수 있게 한다.
3) 저장값과 변경함수를 반환
return [storedValue, setStoredValue];
이건 일반 useState처럼 사용할 수 있도록 만들어준다.
그래서 외부에서 밑에와 같이 쓴다.
const [moneyList, setMoneyList] = useLocalStorage('moneyList', []);
✨ 완성된 화면

💡 프로젝트 이름과 의미
지갑을 지켜보는 사람이라는 의미로 WalletWatcher라고 프로젝트 이름을 만들어봤다.
🦊 GitHub
전체 코드는 여기서 확인할 수 있다 ⬇️
https://github.com/yshls/WalletWatcher
GitHub - yshls/WalletWatcher
Contribute to yshls/WalletWatcher development by creating an account on GitHub.
github.com
🦆 오류 수정 과정
1. 추가 후 잔액(balance)이 업데이트되지 않는 문제
문제
새 항목을 추가했는데 잔액, 수입, 지출 데이터가 반영되지 않았다.
원인
amount를 숫자가 아닌 문자열(String) 로 저장하고 있어서 계산에 실패했다.
해결
onCreate 함수에서 amount를 Number(amount) 또는 parseInt(amount)로 변환해 저장하도록 수정했다.
2. LocalStorage 값 저장/불러오기 문제
문제
새로고침하면 입력한 데이터가 사라졌다.
원인
초기에는 단순 useState만 사용하고 있어서 브라우저에 데이터가 저장되지 않았다.
해결
useLocalStorage 커스텀 훅을 직접 작성해서, 데이터가 입력될 때마다 localStorage에 자동으로 저장되도록 구현했다.
3. 스타일 깨짐 및 반응형 대응 실패
문제
화면 사이즈가 줄어들면 input 요소가 겹치거나 깨졌다.
원인
처음부터 반응형 CSS(Media Query)를 고려하지 않고 스타일을 짰다.
해결
@media (max-width: 768px)를 추가해서 inputField를 flex-direction: column으로 바꾸고 input width를 100%로 채우도록 조정했다.
4. radio 버튼 클릭 불가 및 스타일 문제
문제
커스텀 radio 버튼을 만들다가 클릭이 안되는 문제가 발생했다.
원인
::before 가상 요소를 사용할 때 position과 사이즈를 제대로 설정하지 않아, 클릭 영역을 가려버렸다.
해결
input[type="radio"] 자체의 크기와 ::before의 크기/위치를 세밀하게 조정해서기본 radio 클릭이 정상적으로 작동하게 했다.
🛶 배운점
- toLocaleString()으로 숫자 포맷팅 처리하는 법
- onCreate()처럼 props로 넘긴 함수가 어떻게 호출되는지
- input[type="radio"] 커스터마이징 및 스타일링
- checked 속성의 의미와 초기값 설정 이유
- useLocalStorage 커스텀 훅의 구조와 동작 원리
🎠 회고
과제를 계기로 나에게 하나의 작은 프로젝트가 생겼다. 리디자인을 진행하며 예전에는 막연하게 컴포넌트를 작성했다면 오늘은 고민해보는 경험을 할 수 있었다. 처음에는 공통된 UI를 기준으로 컴포넌트를 나누려했지만, 결국 전체 구조를 4개의 큰 영역으로 나누게 되었고, 그 덕분에 작업이 오히려 수월해졌다.
디자인 작업에는 오전 시간과 오후 전반부의 시간을 대부분 투자했다. 원래 디자인할때 border 없이 진행하는 편이였지만, 수업시간에 선생님께서 border 값을 주며 진행했던 기억이 떠올라 나도 한번 시도해봤다. 그랬더니 padding이나 margin을 조정할때 시각적인 효과를 바로 확인할 수 있어 훨씬 유용하게 작업할 수 있었다.
디자인을 마치고 본격적으로 기능 구현에 들어갔다. "어떻게 구현하지....', '어떤 식으로 작성해야 할지 감이 잡히지 않네...'라는 막막함이 컸지만, 기존에 내가 작성해둔 글(https://recordoftheday.tistory.com/entry/React-PROJ2-TODOLIST-UI%EA%B5%AC%ED%98%84-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84-1)을 참고하며 하나씩 따라해봤다. 그리고 GPT의 가이드를 받으며 기능 구현을 진행하니 기능 구현에 대한 고민이 해결되는 실마리를 찾은 느낌이었다. 리액트 훅에 대한 개념도 매번 헷갈렸는데, 생각보다 반복되는 패턴이 많다는 것을 깨달았다. 특히 useState, useRef, useEffect 같은 기본 훅들을 실제 코드에 적용하면서 언제, 왜 사용하는지 감각적으로 이해할 수 있었다. 단순히 문법을 외우는 것보다, 직접 써보고 수정하는 과정이 가장 큰 공부가 되었다.
프로젝트 내용을 정리하면서 A부터 Z까지 하나하나 의미를 되새기며 작성하다 보니, 그동안 뒤죽박죽 쌓여 있던 리액트 개념들이 점차 체계적으로 정리되는 느낌이었다. 다만, 내일이 지나면 또 잊어버릴 것 같다는 불안감도 든다…
오류는 처음에는 무섭지만, 하나하나 해결하면서 더 빠르게 성장할 수 있다는 걸 느꼈다. 에러 메시지를 무시하지 않고, 문제를 정확히 읽어내고 찾아보는 습관을 길러야 겠다. "작은 문제를 바로 고치는 경험"이 결국 나중에 큰 프로젝트에서의 디버깅 능력으로 이어진다?
진짜 끝!
o(* ̄▽ ̄*)ブ
'💡 URECA > 🗒️ 스터디 노트' 카테고리의 다른 글
[URECA] Day64 React API 공공데이터 (0) | 2025.04.29 |
---|---|
[URECA] Day63 React Weather API (0) | 2025.04.28 |
[URECA] Day 60 React Redux (0) | 2025.04.24 |
[URECA] Day59 React 총정리 🐳 (개념 배울때마다 여기에 추가하기) (0) | 2025.04.23 |
[URECA] Day58 React PROJ Shop URL (0) | 2025.04.22 |