| 강예령 | 남유성 | |
|---|---|---|
| Step1 | cactus-step1 , PR #3 | hippo-step1 , PR #1 |
| Step2 | cactus-step2 , PR #4 | hippo-step2 , PR #2 |
| Step3 | cactus-step3 , PR #8 | hippo-step3 , PR #5 |
| Step4 | cactus-step4 , PR #9 | hippo-step4 , PR #6 |
| Step5 | cactus-step5 , PR #10 | hippo-step5 , PR #7 |
- Component 기본 구조와 JSX
- Props와 State
- 핵심 키워드:
JSX,Props,State
날짜: 2026.05.30 ~ 2026.06.03
1. CSS Module 특수문자 표기법
CSS Module 사용 시 하이픈(-)이 포함된 클래스명을 점 표기법(styles.class-name)으로 접근하여 JavaScript 연산자 오류가 발생했다. 특수문자가 포함된 객체 속성은 대괄호 표기법(styles["class-name"])을 사용해야 한다는 점을 확인하고 코드를 수정했다.
2. 조건부 동적 이미지 렌더링
데이터 카테고리에 따라 다른 이미지를 띄울 때, 초기에는 하드코딩 방식을 사용하여 유지보수성이 떨어지는 문제가 있었다. 이를 개선하기 위해 카테고리명을 키(Key)로 갖는 매핑 객체를 선언하여 조건부 렌더링을 처리하는 방식으로 리팩토링했다.
3. State 끌어올리기
CategoryFilter, RestaurantList가 동일한 상태 변화를 공유해야 할 때, 각자의 State를 지우고 가장 가까운 공통 부모 컴포넌트(App)로 상태를 옮겨서 관리하는 패턴이다. 부모가 상태를 중앙 통제하고 자식들에게 Props로 내려줌으로써, 데이터의 출처를 하나로 통일하고 예측 가능한 단방향 데이터 흐름을 유지할 수 있다.
참고 링크
정적 경로 방식
<img src="../templates/add-button.png" alt="음식점 추가" />빌드 도구의 처리 없이 문자열 경로를 그대로 사용하며, 주로 public 폴더에 위치한 변하지 않는 에셋을 단순 참조할 때 쓴다.
모듈 임포트 방식
import addButton from "../../assets/add-button.png";
<img src={addButton} alt="음식점 추가" />번들러(Webpack, Vite 등)가 파일을 모듈로 처리하여 빌드 시점에 파일 누락을 방지하고, 캐싱 최적화를 위한 해시(hash) 처리가 가능해 더 안전하고 효율적이다.
상태 변경 함수인 setCategory를 자식에게 전달하는 방식
function App() {
const [category, setCategory] = useState("전체");
return (
<>
<Header />
<main>
<CategoryFilter category={category} onChangeCategory={setCategory} />
<RestaurantList restaurants={filteredRestaurants} />
</main>
</>
);
}
export default App;export default function CategoryFilter({ category, onChangeCategory }) {
const handleSelectChange = (e) => {
onChangeCategory(e.target.value);
};
return (
<section className={styles["restaurant-filter-container"]}>
<select
name="category"
id="category-filter"
className={styles["restaurant-filter"]}
aria-label="음식점 카테고리 필터"
value={category}
onChange={handleSelectChange}
>
...
</select>
</section>
);
}부모 컴포넌트의 코드는 간결해지지만, 자식 컴포넌트가 DOM 이벤트 구조(e.target.value)를 직접 알고 가공해야 하므로 자식의 의존성이 약간 높아진다.
부모 컴포넌트(App)에서 핸들러를 정의해서 자식으로 내려주는 방식
function App() {
const [category, setCategory] = useState("전체");
const filteredRestaurants = filterRestaurants(RESTAURANTS, category);
function handleChange(e) {
setCategory(e.target.value);
}
return (
<>
<Header />
<main>
<CategoryFilter category={category} onChangeCategory={handleChange} />
<RestaurantList restaurants={filteredRestaurants} />
</main>
</>
);
}
export default App;부모가 이벤트를 직접 가공하고 자식은 전달받은 함수만 실행하게 하여, 자식 컴포넌트를 더 단순하고 재사용성 높게 유지할 수 있는 권장 패턴이다.
- BEM 원칙 적용 및 CSS Module 접근 표기법 일관성 확보: 엘리먼트는
__, modifier는--로 구분하고, block/element는 점 표기법, modifier는 대괄호 표기법으로 접근 기준을 통일했다.
// block/element: 점 표기법
className={styles.modal__backdrop}
// modifier: 대괄호 표기법
className={styles["modal--open"]}- 조건부 렌더링 활용
- 재사용 가능한 컴포넌트 설계
- 핵심 키워드:
controlled vs uncontrolled
날짜: 2026.06.04 ~ 2026.06.13
1. 무엇을 state로 선언할지 기준
모달에 표시할 선택된 음식점 데이터가 filteredRestaurants처럼 기존 값에서 파생 가능한지, 아니면 별도 state로 선언해야 하는지 헷갈렸다. 실제로는 두 값의 역할이 다르다.
filteredRestaurants: 화면에 보여줄 목록 →category가 정해지면 계산 가능, state 불필요clickedRestaurant: 모달에 보여줄 선택된 하나 → 클릭이 발생해야 비로소 생기는 값, 기존 state로 계산할 수 없으므로 state 필요
2. 어떤 state를 어느 컴포넌트가 소유할지
음식점 추가 기능 구현 시 필요한 state들을 어느 컴포넌트에 선언할지 판단하기 어려웠다. "이 state를 누가 필요로 하는가"를 기준으로 삼았고, 여러 컴포넌트가 함께 필요하면 가장 가까운 공통 부모로 끌어올렸다.
- 음식점 목록 —
RestaurantList(표시)와AddRestaurantModal(추가)이 모두 필요하므로App이 소유한다. - 모달 열림/닫힘 —
Header(열기 버튼)와AddRestaurantModal(조건부 렌더링)이 모두 필요하므로App이 소유한다. - 폼 입력값(카테고리, 이름, 설명) — 제출 전까지
AddRestaurantModal안에서만 쓰이므로AddRestaurantModal이 소유한다.
<li>에 직접 클릭 이벤트 부여
// <li>에 직접 클릭 이벤트 부여
<li
role="button"
tabIndex={0}
onClick={() => onRestaurantClick(restaurant)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
onRestaurantClick(restaurant);
}
}}
><li> 안에 <button> 사용
// <li> 안에 <button> 사용
<li>
<button
className={styles.restaurant__button}
onClick={() => onRestaurantClick(restaurant)}
><li>에 직접 클릭 이벤트를 부여하면 <button> 사용 시 브라우저가 기본으로 처리해주는 것들을 직접 추가해야 한다. 브라우저가 기본으로 처리해주는 것들에는 키보드 포커스, Enter/Space 키 클릭 동작, 스크린 리더 역할 안내 등이 있다. 따라서 <li> 안에 <button>을 사용하는 것이 더 적절하다.
일반 업데이트
setRestaurants([...restaurants, newRestaurant]);
이전 state와 무관한 새 값을 설정할 때 사용한다.
함수형 업데이트
setRestaurants((prev) => [...prev, newRestaurant]);
새 state가 이전 state에 의존할 때, 또는 빠른 연속 업데이트가 예상될 때(타이머, 소켓 이벤트 등) 사용한다. 식당을 추가할 때는 기존 목록을 유지하면서 하나를 추가하는 구조이므로, [...restaurants, newRestaurant]은 이전 restaurants 값에 의존한다. React가 최신 state를 prev로 보장하기 때문에 함수형 업데이트가 적절하다.
강예령
-
CATEGORIES로 상수 추출 후 map으로 옵션 렌더링
{CATEGORIES.map((value) => ( <option key={value} value={value}> {value} </option> ))}
-
crypto.randomUUID()사용해서 id 생성onSubmit({ id: crypto.randomUUID(), category, name, description });
Date.now()는 밀리초 단위 숫자라 짧은 시간 안에 두 번 호출되면 같은 값이 나올 수 있다.crypto.randomUUID()는 브라우저 내장 함수로"550e8400-e29b-41d4-a716-446655440000"형태의 충돌 없는 고유 ID를 생성한다.
남유성
-
!!명시적 boolean 변환: 파생 상태를 변수로 선언해 JSX 의도를 드러내고, 숫자 falsy 렌더링 버그를 방어한다.const isRestaurantDetailModalOpen = !!clickedRestaurant;
-
Modal 래퍼
<div>제거 및 Fragment 변환: 조건부 렌더링으로 CSS 토글이 불필요해졌고,position: fixed요소는 부모 레이아웃에 영향받지 않아 래퍼를 제거했다.// before: <div className={`${styles.modal} ${styles["modal--open"]}`}> // after: <>
- API 요청과 비동기 처리
- 자바스크립트의 싱글 스레드 개념
- 핵심 키워드:
useEffect,useCallback,fetch,async/await,side effect
날짜: 2026.06.08 ~ 2026.06.15
1. useEffect 올바르게 사용하기
async와 cleanup
useEffect 콜백은 cleanup 함수 또는 undefined만 반환해야 한다. async 함수는 항상 Promise를 반환하므로 콜백에 직접 async를 쓸 수 없다. 내부에 async 함수를 따로 선언해 호출하거나, void로 반환값을 undefined로 만들어 해결한다.
cleanup 함수는 컴포넌트가 언마운트될 때 side effect를 정리하기 위해 반환한다. 이벤트 리스너, 타이머처럼 컴포넌트가 사라져도 계속 살아있는 것들은 직접 제거해줘야 한다.
useEffect(() => {
document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick);
}, []);이번 미션에서 void와 cleanup은 필수가 아니었다. void는 lint 경고를 없애기 위한 것이고, API 호출은 요청 후 끝나는 일회성 작업이라 정리할 것 자체가 없다. 어떤 상황에서 필요한지 기준을 잡는 게 어려웠다.
의존성과 useCallback
useEffect 내부에서 사용하는 함수를 의존성 배열에 넣어야 하는지는 선언 위치에 따라 달라진다.
useEffect 안에서 선언+호출을 모두 하면 함수가 밖으로 나가지 않으므로 의존성 배열에 넣을 필요가 없다.
useEffect(() => {
const fetchRestaurants = async () => { ... }; // 선언
fetchRestaurants(); // 호출
}, []); // 의존성 배열에 안 넣어도 됨밖에서 선언하면 useEffect가 그 함수에 의존하게 되므로 의존성 배열에 넣어야 한다. 그런데 함수는 리렌더링마다 새 참조가 만들어지므로 그냥 넣으면 무한루프가 생긴다. useCallback으로 참조를 고정해야 의존성 배열에 안전하게 넣을 수 있다.
// 밖에서 선언 → 의존성 배열에 넣어야 함 → 무한루프
const fetchRestaurants = async () => { ... };
useEffect(() => { fetchRestaurants(); }, [fetchRestaurants]);
// useCallback으로 참조 고정 → 해결
const fetchRestaurants = useCallback(async () => { ... }, []);
useEffect(() => { void fetchRestaurants(); }, [fetchRestaurants]);3. 커스텀 훅 분리 기준
"이걸 훅으로 빼야 하나?"를 판단하는 기준이 처음엔 모호했다. 실무에서 주로 쓰는 기준은 세 가지다.
- 재사용: 같은 state + effect 조합을 두 곳 이상에서 쓸 때
- 복잡도: 컴포넌트에 state/effect/핸들러가 많아 읽기 어려울 때
- 관심사 분리: "데이터를 어떻게 다루는가"와 "UI를 어떻게 보여주는가"를 나누고 싶을 때
이번 미션에서 useRestaurants를 만든 이유는 세 번째에 해당한다. App이 데이터를 어떻게 불러오는지 몰라도 결과만 받아 렌더링에 집중할 수 있도록 분리했다. 단순히 로직을 줄이려는 게 아니라, 컴포넌트의 역할을 명확히 하기 위한 분리다.
useState lazy initialization
개인 미션 진행 중 useState(fetchRestaurants)처럼 함수를 초기값으로 넘겨봤다가 렌더링이 깨지는 오류가 났다. 처음엔 배열이 아닌 객체가 들어가서 생긴 문제라고 생각했는데, 스터디 세션에서 이야기하며 찾아보니 다른 원인이었다.
React는 useState에 함수를 넘기면 lazy initialization으로 처리한다. 값 자체가 아니라 함수를 넘기면 첫 렌더링 때 그 함수를 호출하고 반환값을 초기값으로 사용한다. fetchRestaurants는 async 함수라 호출하면 Promise를 반환하고, state에 Promise가 들어가 .map()을 쓸 수 없어 렌더링이 깨진 것이었다.
useState(fetchRestaurants)
// → fetchRestaurants() 호출 → Promise 반환 → state 초기값 = Promiselazy initialization 자체는 버그가 아니라 최적화 패턴이다. 초기값 계산 비용이 클 때 첫 렌더링에만 한 번 실행되도록 최적화할 수 있다.
// 매 렌더링마다 localStorage를 읽음
useState(localStorage.getItem("key") ?? "")
// 첫 렌더링에만 한 번 읽음
useState(() => localStorage.getItem("key") ?? "")