Skip to content

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;

디렉토리 구조와 디자인 패턴은 조화되는 개념이지, 상충되는 개념이 아니다