0909
FSD (feat. 디렉토리 구조 다시 생각하기)
- app: 애플리케이션 진입점
- widgets: 독립적인 ui 컴포넌트 덩어리
- features: 특정 비즈니스 기능
- entities: 도메인 모델과 관련된 데이터 페칭
- shared: 모든 계층에서 사용되는 유틸
- app부터 상위 계층 -> shared로 갈 수록 하위 계층
- 상위 계층은 하위 계층을 참조할 수 있지만
- 하위 계층은 상위 계층을 참조할 수 없다
오늘의 오류
- nickname 입력 필드에
maxLength를 설정했지만, 숫자나 영어와는 다르게 한글은 한 글자가 더 입력되는 버그가 발생 - 한글은 일반적으로 2바이트를 차지하는 반면, 숫자나 영어는 1바이트. HTML의
maxLength속성은 이 바이트 수를 고려하지 않고 글자(character)의 개수만을 세기 때문에 이러한 문제가 발생. - 고로 입력 이벤트를 감지하는 onChange 핸들러에서 직접 입력 길이를 검사하고, 초과할 경우 입력을 막는 로직을 추가해야함. 스키마 유효성 검사도 중요하지만, UX측면에서 아예 입력 단계부터 초과를 막는 것이 더 좋은 사용자 경험을 제공.
- 관련 자료: https://jaysheen.tistory.com/16
순환 참조
옵저버 패턴
- 주체(Subject)의 상태 변화를 관찰자(Observer)들이 구독하여, 주체가 변경될 때마다 알림을 받는 방식
js
// auth-context.js (Subject 역할)
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const login = () => setIsLoggedIn(true);
const logout = () => setIsLoggedIn(false);
return (
<AuthContext.Provider value={{ isLoggedIn, login, logout }}>
{children}
</AuthContext.Provider>
);
};
// useAuth.js (Custom Hook)
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
// Header.js (Observer 역할)
import { useAuth } from './auth/useAuth';
export const Header = () => {
const { isLoggedIn, login, logout } = useAuth();
return (
<header style={{ padding: '10px', borderBottom: '1px solid #ccc' }}>
<h1>My App</h1>
{isLoggedIn ? (
<button onClick={logout}>로그아웃</button>
) : (
<button onClick={login}>로그인</button>
)}
</header>
);
};
// App.js
import { AuthProvider } from './auth/auth-context';
import Header from './components/Header';
import UserInfo from './components/UserInfo';
const App = () => {
return (
<AuthProvider>
<Header />
</AuthProvider>
);
};
export default App;메디터 패턴
- 여러 컴포넌트들이 서로 직접 통신하지 않고, 중재자(Mediator) 역할을 하는 객체를 통해서만 소통하는 방식
js
// useMediator.js (Mediator 역할)
import { useState, useCallback } from 'react';
export const useMediator = () => {
const [data, setData] = useState(null);
const handleButtonClick = useCallback((dataType) => {
if (dataType === 'A') {
setData('A 컴포넌트에서 데이터 요청');
} else if (dataType === 'B') {
setData('B 컴포넌트에서 데이터 요청');
}
}, []);
const resetData = useCallback(() => {
setData(null);
}, []);
return {
data,
handleButtonClick,
resetData,
};
};
// ComponentA.js
export const ComponentA = ({ onAction }) => {
return (
<button onClick={() => onAction('A')}>A 컴포넌트: 데이터 요청</button>
);
};
// ComponentB.js
const ComponentB = ({ onAction }) => {
return (
<button onClick={() => onAction('B')}>B 컴포넌트: 데이터 요청</button>
);
};
export default ComponentB;
// Display.js
// Props로 중재자의 데이터를 전달받음
const Display = ({ data }) => {
return (
<div style={{ margin: '20px' }}>
<p>받은 데이터: {data || '없음'}</p>
</div>
);
};
export default Display;
// App.js
import { useMediator } from './useMediator';
import ComponentA from './ComponentA';
import Display from './Display';
const App = () => {
const { data, handleButtonClick, resetData } = useMediator();
return (
<div style={{ padding: '20px' }}>
<ComponentA onAction={handleButtonClick} />
<ComponentB onAction={handleButtonClick} />
<button onClick={resetData}>데이터 초기화</button>
<Display data={data} />
</div>
);
};
export default App;