3๋…„๊ฐ„ ํšŒ์‚ฌ์—์„œ ๊ฐœ๋ฐœํ•˜๋ฉด์„œ ๋‚˜๋ฆ„ ๊ฒฝํ—˜์„ ์Œ“์•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๋Œ์ด์ผœ๋ณด๋ฉด ๊ทธ ๊ฒฝํ—˜์€ ์ด๋ฏธ ์ž˜ ์งœ์ธ ๊ตฌ์กฐ ์•ˆ์—์„œ ์ฃผ์–ด์ง„ ์—ญํ• ๋งŒ ์ˆ˜ํ–‰ํ–ˆ์—ˆ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.
๋””์ž์ด๋„ˆ์™€์˜ ํ˜‘์—… ๋ฐฉ์‹์€ ์ด๋ฏธ ์ •ํ˜•ํ™”๋˜์–ด ์žˆ์—ˆ๊ณ , ๋””์ž์ธ ์‹œ์Šคํ…œ๋„ ๊ตฌ์ถ•๋ผ ์žˆ์–ด์„œ ์‹œ์•ˆ์„ ๋ฐ›์•„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ๋งŒ ์ง‘์ค‘ํ•˜๋ฉด ๋๊ฑฐ๋“ ์š”.

์•Œ๋ฆฌ๋‹ˆ ์‹์‚ฌ ํ™”๋ฉด

 

 

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ชจ๋“  ๊ฑธ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค์–ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๊ณผ์ •์—์„œ ๋งŽ์€ ๊ฒƒ๋“ค์„ ์ƒˆ๋กญ๊ฒŒ ๊นจ๋‹ฌ์•˜๊ณ , ํŠนํžˆ ๋””์ž์ด๋„ˆ์™€์˜ ํ˜‘์—…์—์„œ ์—ฌ๋Ÿฌ ๋ฐฐ์›€๋“ค์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค!

 

๊ฐ‘์ž๊ธฐ ๋ณด์ด๊ธฐ ์‹œ์ž‘ํ•œ ๊ฒƒ๋“ค

์ƒํƒœ์— ๋Œ€ํ•œ ์ •์˜

๋””์ž์ด๋„ˆ๋‹˜๊ณผ ๊ฐ™์€ "์ƒํƒœ"๋ผ๋Š” ๋‹จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ์‹ค์ œ๋กœ๋Š” ๋‹ค๋ฅธ ๋ฐฉ์‹์„ ์ƒ๊ฐํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • ๋””์ž์ด๋„ˆ๋‹˜์ด ๋งํ•˜๋Š” ์ƒํƒœ๋Š” ์ฃผ๋กœ ์‹œ๊ฐ์ ์ธ ๋ณ€ํ™” — ์˜ˆ๋ฅผ ๋“ค์–ด ์ƒ‰๊น”, ํ…Œ๋‘๋ฆฌ, ์•„์ด์ฝ˜ ๊ฐ™์€ UI ์š”์†Œ๋“ค์ด์—ˆ๊ณ ,
  • ์ œ๊ฐ€ ๋งํ•˜๋Š” ์ƒํƒœ๋Š” ๋ฐ์ดํ„ฐ์˜ ์ƒํƒœ ํ˜น์€ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ — ์˜ˆ๋ฅผ ๋“ค์–ด ์—๋Ÿฌ ์—ฌ๋ถ€, ์ž…๋ ฅ ์™„๋ฃŒ ์—ฌ๋ถ€, ๋„คํŠธ์›Œํฌ ์ƒํƒœ ๊ฐ™์€ ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜์—์„œ ์ž์„ธํžˆ ์„ค๋ช… ํ•˜๊ฒ ์ง€๋งŒ ๊ฐœ๋ฐœํ–ˆ๋˜ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๋กœ ์„ค๋ช…ํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ฒ˜์Œ ์‹œ์•ˆ์—๋Š” "๊ธฐ๋ณธ, ํฌ์ปค์Šค, ์—๋Ÿฌ, ๋น„ํ™œ์„ฑํ™”" ์ •๋„๋งŒ ์ •์˜๋ผ ์žˆ์—ˆ์–ด์š”.

๊ทธ๋Ÿฐ๋ฐ ์‹ค์ œ๋กœ ๊ตฌํ˜„์— ๋“ค์–ด๊ฐ€๋ฉด์„œ ๊ณ ๋ฏผ์ด ์ปค์กŒ์Šต๋‹ˆ๋‹ค.

 

  • ์ด๋ฉ”์ผ ํ•„๋“œ์—์„œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋Š” ๋™์‹œ์— ํฌ์ปค์Šค๊ฐ€ ๊ฐ€๋ฉด ์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์ค˜์•ผ ํ•˜์ง€?
  • ๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด์„œ ๋™์‹œ์— ๋กœ๋”ฉ ์ƒํƒœ๋„ ์œ ์ง€ํ•ด์•ผ ํ•œ๋‹ค๋ฉด?
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ์—์„œ๋Š” ํ˜ธ๋ฒ„ + ์—๋Ÿฌ ์ƒํƒœ๊ฐ€ ํ•จ๊ป˜ ์˜ค๋ฉด ์–ด๋–ค UI์—ฌ์•ผ ํ• ๊นŒ?

์ด๋ ‡๊ฒŒ ์ƒํƒœ ์กฐํ•ฉ ๋งŽ์•„์ง€๊ธฐ ์‹œ์ž‘ํ–ˆ์–ด์š”.
ํšŒ์‚ฌ๋Š” ์ด๋ฏธ ๋””์ž์ธ ์‹œ์Šคํ…œ๊ณผ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž˜ ์ •๋ฆฌ๋ผ ์žˆ์–ด์„œ, ์ด๋Ÿฐ ์กฐํ•ฉ๋“ค์€ ์‹ ๊ฒฝ ์“ธ ํ•„์š”๊ฐ€ ๊ฑฐ์˜ ์—†์—ˆ๊ฑฐ๋“ ์š”.
ํ•˜์ง€๋งŒ ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋ชจ๋“  ๊ฒƒ์„ ์ง์ ‘ ๊ธฐํšํ•˜๊ณ , ๋””์ž์ด๋„ˆ๋‹˜๊ณผ ๋…ผ์˜ํ•˜๊ณ , ๊ตฌํ˜„๊นŒ์ง€ ํ•ด๋ณด๋ฉด์„œ "์ƒํƒœ"๋ฅผ ์ •์˜ํ•œ๋‹ค๋Š”๊ฒŒ ์–ผ๋งˆ๋‚˜ ๋ณต์žกํ•˜๊ณ  ์„ฌ์„ธํ•œ ์ผ์ธ์ง€ ์ฒด๊ฐํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

 

๊ฐ€์žฅ ์ฒ˜์Œ ์‹œ์ž‘๋œ ์†Œํ†ต์ด ์‹œ์ž‘๋œ ํ™”๋ฉด

๋กœ๊ทธ์ธ ํ™”๋ฉด

 

ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐ ๋””์ž์ด๋„ˆ๋‹˜์ด ๋กœ๊ทธ์ธ ํ™”๋ฉด ์‹œ์•ˆ์„ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค. ๊น”๋”ํ•˜๊ณ  ์ง๊ด€์ ์ธ ๋””์ž์ธ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๊ตฌํ˜„์„ ์‹œ์ž‘ํ•˜๋ฉด์„œ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ช‡ ๊ฐ€์ง€ ๊ถ๊ธˆํ•œ ์ ๋“ค์ด ์ƒ๊ฒผ์–ด์š”.

 

"๋กœ๊ทธ์ธ ์‹คํŒจํ–ˆ์„ ๋•Œ๋Š” ์–ด๋–ป๊ฒŒ ํ‘œ์‹œํ• ๊นŒ์š”?"

 

๋””์ž์ด๋„ˆ๋‹˜๋„ "์•„, ๊ทธ๋Ÿฌ๊ฒŒ์š”. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ํ•„์š”ํ•˜๊ฒ ๋„ค์š”"๋ผ๊ณ  ํ•ด์ฃผ์…จ๊ณ  ํ•จ๊ป˜ ๊ณ ๋ฏผํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

 

 

๋กœ๊ทธ์ธ ์—๋Ÿฌ ํ™”๋ฉด ์˜ˆ์‹œ

 

์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ ์ฝ”๋“œ์™€ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

type LoginErrorType = 'INVALID_CREDENTIALS' | 'NETWORK_ERROR' | 'UNKNOWN_ERROR';

interface LoginError {
  type: LoginErrorType;
  message: string;
  field?: 'email' | 'password'; // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ
}

interface LoginState {
  status: 'idle' | 'loading' | 'success' | 'error';
  error: LoginError | null;
}

// ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์ˆ˜ ๋ถ„๋ฆฌ
const LOGIN_ERROR_MESSAGES = {
  INVALID_CREDENTIALS: '์ด๋ฉ”์ผ ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
  NETWORK_ERROR: '๋„คํŠธ์›Œํฌ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.',
  UNKNOWN_ERROR: '์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.',
} as const;

// ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์šฉ ์ƒ์„ธ ๋ฉ”์‹œ์ง€
const DEV_ERROR_MESSAGES = {
  INVALID_EMAIL: '์•—! ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ์ด๋ฉ”์ผ์ด๊ฑฐ๋‚˜ ์ž˜๋ชป ์ž…๋ ฅํ–ˆ์–ด์š”.',
  INVALID_PASSWORD: '์•—! ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ํ‹€๋ ธ์–ด์š”. ๋‹ค์‹œ ํ•œ ๋ฒˆ ํ™•์ธํ•ด์ฃผ์„ธ์š”.',
} as const;

const useLogin = () => {
  const [state, setState] = useState<LoginState>({
    status: 'idle',
    error: null,
  });

  // ํ™˜๊ฒฝ๋ณ„ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
  const getErrorMessage = (type: LoginErrorType, field?: 'email' | 'password'): LoginError => {
    const isDevelopment = process.env.NODE_ENV === 'development';
    
    if (isDevelopment && field) {
      return {
        type,
        message: field === 'email' ? DEV_ERROR_MESSAGES.INVALID_EMAIL : DEV_ERROR_MESSAGES.INVALID_PASSWORD,
        field,
      };
    }

    return {
      type,
      message: LOGIN_ERROR_MESSAGES[type],
    };
  };

  const login = async (email: string, password: string) => {
    setState({ status: 'loading', error: null });
    
    try {
      const user = await authService.findUserByEmail(email);
      if (!user) {
        setState({
          status: 'error',
          error: getErrorMessage('INVALID_CREDENTIALS', 'email'),
        });
        return;
      }

      const isValidPassword = await authService.validatePassword(email, password);
      if (!isValidPassword) {
        setState({
          status: 'error',
          error: getErrorMessage('INVALID_CREDENTIALS', 'password'),
        });
        return;
      }

      await authService.setSession(user);
      setState({ status: 'success', error: null });
    } catch (error) {
      setState({
        status: 'error',
        error: getErrorMessage('NETWORK_ERROR'),
      });
    }
  };

  return {
    ...state,
    login,
    isLoading: state.status === 'loading',
    isSuccess: state.status === 'success',
    isError: state.status === 'error',
  };
};

 

๋””์ž์ด๋„ˆ๋‹˜์€ ๋กœ๊ทธ์ธ ํ™”๋ฉด์—์„œ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šธ์ง€ ๊ณ ๋ฏผํ•˜๊ณ  ๊ณ„์…จ์–ด์š”.

 

ํ”ํžˆ ๋ณผ ์ˆ˜ ์žˆ๋Š” "์•„์ด๋””๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"์™€ ๊ฐ™์€ ๋ชจํ˜ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋Œ€์‹ , "์ด๋ฉ”์ผ์ด ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค" ๋˜๋Š” "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค"์ฒ˜๋Ÿผ ๊ตฌ์ฒด์ ์œผ๋กœ ์•Œ๋ ค์ฃผ๋Š” ๊ฒŒ ์‚ฌ์šฉ์ž ์ž…์žฅ์—์„œ ๋” ์นœ์ ˆํ•œ UX๊ฐ€ ์•„๋‹๊นŒ์— ๋Œ€ํ•ด ๋…ผ์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ  ์™œ ๋งŽ์€ ์›น์‚ฌ์ดํŠธ๋“ค์ด ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ "Incorrect username or password" ์ฒ˜๋Ÿผ ๋ชจํ˜ธํ•œ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฑธ๊นŒ์š”? ๋ผ๋Š” ์˜๋ฌธ์— ๋„๋‹ฌํ–ˆ์–ด์š”.

 

๊ด€๋ จํ•ด์„œ ์ฐพ์•„๋ณธ ๊ฒฐ๊ณผ, tarunbatra.com ๋ธ”๋กœ๊ทธ์—์„œ๋Š” ๋ณด์•ˆ์ ์ธ ์ด์œ ๋ฅผ ๊ฐ•์กฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๋กœ๊ทธ์ธ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์ฒด์ ์œผ๋กœ ์ œ๊ณตํ•  ๊ฒฝ์šฐ, ์•…์˜์ ์ธ ์‚ฌ์šฉ์ž๊ฐ€ ํ•ด๋‹น ์ •๋ณด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์œ ํšจํ•œ ๊ณ„์ •์„ ์ถ”์ธกํ•˜๊ฑฐ๋‚˜ ๋ฐ˜๋ณต์ ์œผ๋กœ ์กฐํ•ฉ์„ ์‹œ๋„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด, "๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค"๋ผ๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋ฉด ํ•ด๋‹น ์‚ฌ์šฉ์ž๋ช…์€ ์œ ํšจํ•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ณต๊ฒฉ์ž์—๊ฒŒ ๋‹จ์„œ๋ฅผ ์ฃผ๋Š” ์…ˆ์ด์ฃ . ๋ฐ˜๋ฉด "์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"์ฒ˜๋Ÿผ ๋ชจํ˜ธํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋ฉด ๊ณต๊ฒฉ์ž๋Š” ์–ด๋–ค ๋ถ€๋ถ„์ด ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์— ๋ณด์•ˆ์ƒ ๋” ์•ˆ์ „ํ•ด์ง‘๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ๊ทธ ๋‹น์‹œ์—” ๋‚ด๋ถ€ ๋ฒ ํƒ€ ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•  ์˜ˆ์ •์ด์—ˆ๋˜ ๋™๋ฌผ๋ณ‘์› ๊ด€๊ณ„์ž๋ถ„๋“ค์˜ ์š”์ฒญ์ด ์žˆ์—ˆ๊ณ , ํ…Œ์ŠคํŠธ ํŽธ์˜๋ฅผ ์œ„ํ•ด ๋กœ๊ทธ์ธ ์˜ค๋ฅ˜ ์‹œ ์–ด๋–ค ํ•„๋“œ๊ฐ€ ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€๋ฅผ ๋ถ„๋ฆฌํ•ด์„œ ๋ณด์—ฌ์ฃผ๋Š” UX๋ฅผ ์ฑ„ํƒํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ํ•„๋“œ์—์„œ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ๋Š”์ง€ ์ง๊ด€์ ์œผ๋กœ ์•Œ ์ˆ˜ ์žˆ๋„๋ก, ํ•ด๋‹น ํ•„๋“œ ๋ฐ”๋กœ ์•„๋ž˜์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋„์šฐ๋Š” ๋ฐฉ์‹์œผ๋กœ ํ˜‘์˜ํ–ˆ๊ณ , ์‹ค์ œ ์‚ฌ์šฉ์ž ํ…Œ์ŠคํŠธ์—์„œ๋„ ๊ธ์ •์ ์ธ ๋ฐ˜์‘์„ ์–ป์„ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค!

 

 

ํŽธํ•˜๊ฒŒ ์“ฐ๋˜ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ

์•Œ๋ฆฌ๋‹ˆ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์ž…๋ ฅ ํ•„๋“œ๊ฐ€ ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฉ”์ผ, ๋น„๋ฐ€๋ฒˆํ˜ธ, ์ „ํ™”๋ฒˆํ˜ธ, ์‚ฌ๋ฃŒ๋ช…, ๋ธŒ๋žœ๋“œ๋ช… ๋“ฑ๋“ฑ...

 

์ฒ˜์Œ์— ๋””์ž์ด๋„ˆ๋‹˜์ด ๋งŒ๋“  ํ…์ŠคํŠธํ•„๋“œ๋Š” ๊ธฐ๋ณธ์ ์ด ๋ชจ์Šต์ด์—ˆ๋Š”๋ฐ, ๊ตฌํ˜„ํ•˜๋ฉด์„œ ํ•จ๊ป˜ ๋‹ค์–‘ํ•œ ์ƒํƒœ๋ฅผ ๊ณ ๋ฏผํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

"๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ์—์„œ ์ž…๋ ฅ ๋‚ด์šฉ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๋ˆˆ ๋ชจ์–‘ ์•„์ด์ฝ˜์ด ์žˆ์œผ๋ฉด ์ข‹๊ฒ ์–ด์š”."

 

"๋งž์•„์š”! ๊ทธ๋ฆฌ๊ณ  ์‚ฌ๋ฃŒ ๋“ฑ๋กํ•  ๋•Œ ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์€ ๋ผ๋ฒจ์— ๋ณ„ํ‘œ(*)๋ฅผ ํ‘œ์‹œํ•˜๋ฉด ์–ด๋–จ๊นŒ์š”?"

 

์ด์ฒ˜๋Ÿผ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜๋ฉฐ ํ•„์š”ํ•œ ์ƒํƒœ์˜ ์ˆ˜๋Š” ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ๋Š˜์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค..

์˜ˆ๋ฅผ๋“ค์–ด ์•„๋ž˜์ฒ˜๋Ÿผ์š”. (์‹ค์ œ ์•Œ๋ฆฌ๋‹ˆ ๊ตฌํ˜„๊ณผ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค)

  • ๊ธฐ๋ณธ ์ƒํƒœ 4๊ฐ€์ง€
  • ํ˜ธ๋ฒ„ ์ƒํƒœ ์ถ”๊ฐ€ → 4 × 2 = 8
  • ์ฝ๊ธฐ ์ „์šฉ(read-only) ๊ณ ๋ ค → 8 × 2 = 16
  • ํ•„์ˆ˜/์„ ํƒ ํ‘œ์‹œ → 16 × 2 = ์ด 32๊ฐ€์ง€ ์ƒํƒœ ์กฐํ•ฉ

๊ทธ๋Ÿฐ๋ฐ ๊ฐ€์žฅ ์–ด๋ ค์› ๋˜ ๊ฑด ์ด๋Ÿฐ ์กฐํ•ฉ๋“ค์ด ๋‹จ์ˆœํžˆ ์ถ”๊ฐ€๋งŒ ๋˜๋Š” ๊ฒŒ ์•„๋‹ˆ๋ผ, ์„œ๋กœ ์ถฉ๋Œํ•˜๊ฑฐ๋‚˜ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์žˆ๋‹ค๋Š” ์ ์ด์—ˆ์–ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด disabled์™€ readOnly๋Š” ๋™์‹œ์— true์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, ์‹ค์ œ๋กœ๋Š” disabled๊ฐ€ readOnly๋ฅผ ๋ฌด์‹œํ•ด์š”.

์ฆ‰, disabled > readOnly์ธ ๊ฑฐ์ฃ .
์ด๋Ÿฐ ์ƒํ˜ธ ์˜์กด์„ฑ๊ณผ ์šฐ์„ ์ˆœ์œ„๋Š” ์ฒ˜์Œ์—” ์ „ํ˜€ ์ธ์ง€ํ•˜์ง€ ๋ชปํ–ˆ๊ณ , ๋””์ž์ด๋„ˆ๋‹˜๊ณผ ํ•จ๊ป˜ ํ•œ์ฐธ์„ ๋…ผ์˜ํ•˜๋ฉด์„œ ๊ทœ์น™์„ ์ •๋ฆฌํ•ด๋‚˜๊ฐ”์Šต๋‹ˆ๋‹ค:

  • disabled > readOnly (์šฐ์„ ์ˆœ์œ„ ์กด์žฌ)
  • error์™€ success๋Š” ์ƒํ˜ธ ๋ฐฐํƒ€์ 
  • focus๋Š” ์‚ฌ์šฉ์ž์˜ ์ง์ ‘์ ์ธ ์ƒํ˜ธ์ž‘์šฉ์œผ๋กœ๋งŒ ๋ฐœ์ƒ

์ธํ’‹ ์˜ˆ์‹œ ํ™”๋ฉด

 

์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ ์ฝ”๋“œ์™€ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

import { forwardRef, useState, useCallback, useMemo, useId } from 'react';

// ๋””์ž์ธ ํ† ํฐ
const INPUT_COLORS = {
  default: {
    border: '#e1e5e9',
    background: '#ffffff',
  },
  focused: {
    border: '#00b896', // ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ
    background: '#ffffff',
  },
  error: {
    border: '#ff6b6b', // ๋นจ๊ฐ„์ƒ‰
    background: '#ffffff',
  },
  disabled: {
    border: '#f4f4f4',
    background: '#f9f9f9',
  },
} as const;

// ์ƒํƒœ ์šฐ์„ ์ˆœ์œ„ ๋ช…์‹œ์  ์ •์˜
const INPUT_STATE_PRIORITY = {
  disabled: 4,
  error: 3,
  focused: 2,
  default: 1,
} as const;

type InputState = keyof typeof INPUT_STATE_PRIORITY;

// Props ํƒ€์ž… ๋ถ„๋ฆฌ
interface BaseInputProps {
  label?: string;
  required?: boolean;
  error?: string;
  disabled?: boolean;
  placeholder?: string;
  value?: string;
  onChange?: (value: string) => void;
  onFocus?: () => void;
  onBlur?: () => void;
}

interface TextInputProps extends BaseInputProps {
  type?: 'text' | 'email' | 'tel';
  leftIcon?: React.ReactNode;
  rightButton?: React.ReactNode;
}

interface PasswordInputProps extends BaseInputProps {
  type: 'password';
  showPasswordToggle?: boolean;
}

type InputProps = TextInputProps | PasswordInputProps;

// ์ƒํƒœ ๊ณ„์‚ฐ ๋กœ์ง ๋ถ„๋ฆฌ
const getInputState = (isFocused: boolean, error?: string, disabled?: boolean): InputState => {
  if (disabled) return 'disabled';
  if (error) return 'error';
  if (isFocused) return 'focused';
  return 'default';
};

const getInputStyles = (state: InputState) => {
  const colors = INPUT_COLORS[state];
  return {
    borderColor: colors.border,
    backgroundColor: colors.background,
  };
};

const TextInput = forwardRef<HTMLInputElement, InputProps>(({
  label,
  required = false,
  type = 'text',
  error,
  disabled = false,
  placeholder,
  value,
  onChange,
  onFocus,
  onBlur,
  ...props
}, ref) => {
  const [showPassword, setShowPassword] = useState(false);
  const [isFocused, setIsFocused] = useState(false);
  
  const inputId = useId();
  const errorId = useId();
  
  const inputType = useMemo(() => {
    if (type === 'password' && !showPassword) return 'password';
    return type === 'password' ? 'text' : type;
  }, [type, showPassword]);

  const currentState = useMemo(() => 
    getInputState(isFocused, error, disabled), 
    [isFocused, error, disabled]
  );

  const inputStyles = useMemo(() => 
    getInputStyles(currentState), 
    [currentState]
  );

  // ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋“ค
  const handleFocus = useCallback(() => {
    if (disabled) return;
    setIsFocused(true);
    onFocus?.();
  }, [disabled, onFocus]);

  const handleBlur = useCallback(() => {
    setIsFocused(false);
    onBlur?.();
  }, [onBlur]);

  const togglePasswordVisibility = useCallback(() => {
    setShowPassword(prev => !prev);
  }, []);

  const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    onChange?.(e.target.value);
  }, [onChange]);

  // ํŒจ์Šค์›Œ๋“œ ํ† ๊ธ€ ๋ฒ„ํŠผ
  const passwordToggleButton = type === 'password' && (
    <button
      type="button"
      onClick={togglePasswordVisibility}
      className="password-toggle"
      aria-label={showPassword ? '๋น„๋ฐ€๋ฒˆํ˜ธ ์ˆจ๊ธฐ๊ธฐ' : '๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณด๊ธฐ'}
      disabled={disabled}
      style={{ 
        opacity: disabled ? 0.5 : 1,
        cursor: disabled ? 'not-allowed' : 'pointer'
      }}
    >
      {showPassword ? '๐Ÿ™ˆ' : '๐Ÿ‘๏ธ'}
    </button>
  );

  return (
    <div className="text-input">
      {label && (
        <label htmlFor={inputId} className="input-label">
          {label}
          {required && (
            <span className="required-indicator" aria-label="ํ•„์ˆ˜" style={{ color: '#ff6b6b' }}>
              *
            </span>
          )}
        </label>
      )}
      
      <div 
        className={`input-container input-container--${currentState}`}
        style={{
          ...inputStyles,
          border: `1px solid ${inputStyles.borderColor}`,
          borderRadius: '8px',
          padding: '12px',
          display: 'flex',
          alignItems: 'center',
          gap: '8px',
        }}
      >
        {'leftIcon' in props && props.leftIcon && (
          <span className="input-icon" aria-hidden="true">
            {props.leftIcon}
          </span>
        )}
        
        <input
          ref={ref}
          id={inputId}
          type={inputType}
          disabled={disabled}
          placeholder={placeholder}
          value={value}
          onChange={handleChange}
          onFocus={handleFocus}
          onBlur={handleBlur}
          aria-describedby={error ? errorId : undefined}
          aria-invalid={!!error}
          style={{
            flex: 1,
            border: 'none',
            outline: 'none',
            backgroundColor: 'transparent',
            fontSize: '16px',
          }}
        />
        
        {passwordToggleButton}
        
        {'rightButton' in props && props.rightButton}
      </div>
      
      {error && (
        <span 
          id={errorId} 
          className="error-message" 
          role="alert"
          style={{
            color: '#ff6b6b',
            fontSize: '14px',
            marginTop: '4px',
            display: 'block',
          }}
        >
          {error}
        </span>
      )}
    </div>
  );
});

TextInput.displayName = 'TextInput';

// ์‚ฌ์šฉ ์˜ˆ์‹œ
const LoginForm = () => {
  const [formData, setFormData] = useState({
    email: '',
    password: '',
  });
  
  const { login, isLoading, error, status } = useLogin();
  
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    await login(formData.email, formData.password);
  };
  
  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px', margin: '0 auto' }}>
      <TextInput
        label="์ด๋ฉ”์ผ"
        type="email"
        required
        value={formData.email}
        onChange={(email) => setFormData(prev => ({ ...prev, email }))}
        error={error?.field === 'email' ? error.message : undefined}
        placeholder="์ด๋ฉ”์ผ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        disabled={isLoading}
      />
      
      <div style={{ marginTop: '16px' }}>
        <TextInput
          label="๋น„๋ฐ€๋ฒˆํ˜ธ"
          type="password"
          required
          value={formData.password}
          onChange={(password) => setFormData(prev => ({ ...prev, password }))}
          error={error?.field === 'password' ? error.message : undefined}
          placeholder="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
          disabled={isLoading}
          showPasswordToggle
        />
      </div>
      
      <button 
        type="submit" 
        disabled={isLoading}
        style={{
          width: '100%',
          padding: '12px',
          marginTop: '24px',
          backgroundColor: isLoading ? '#cccccc' : '#00b896',
          color: 'white',
          border: 'none',
          borderRadius: '8px',
          fontSize: '16px',
          cursor: isLoading ? 'not-allowed' : 'pointer',
        }}
      >
        {isLoading ? '๋กœ๊ทธ์ธ ์ค‘...' : '๋กœ๊ทธ์ธ'}
      </button>
      
      {error && !error.field && (
        <div style={{ 
          marginTop: '16px', 
          padding: '12px', 
          backgroundColor: '#ffebee',
          borderRadius: '8px',
          color: '#ff6b6b'
        }}>
          {error.message}
        </div>
      )}
    </form>
  );
};

 

"๋น„ํ™œ์„ฑํ™” ์ƒํƒœ์ผ ๋•Œ๋Š” ํšŒ์ƒ‰์œผ๋กœ, ์—๋Ÿฌ ์ƒํƒœ์ผ ๋•Œ๋Š” ๋นจ๊ฐ„์ƒ‰ ํ…Œ๋‘๋ฆฌ๋กœ ํ‰๋ฒ”ํ•˜๊ฒŒ ๊ฐ€๊ณ  ํฌ์ปค์Šค ์ƒํƒœ์—์„œ๋Š” ๋ธŒ๋žœ๋“œ ์ปฌ๋Ÿฌ์ธ ์ดˆ๋ก์ƒ‰์œผ๋กœ ๊ฐ•์กฐํ•˜๋ฉด ์ข‹๊ฒ ์–ด์š”"๋ผ๋Š” ์˜๊ฒฌ์œผ๋กœ ์ผ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

 

์•Œ๋ ˆ๋ฅด๊ธฐ ๊ธฐ๋ก์˜ ์‹œ๊ฐํ™”

์•Œ๋ฆฌ๋‹ˆ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐ˜๋ ค๊ฒฌ์˜ ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์„ ๊ธฐ๋กํ•˜๊ณ  ๋ถ„์„ํ•˜๋Š” ๊ฑด๋ฐ, ์ด ๋ถ€๋ถ„์—์„œ ์ •๋ง ๋งŽ์€ ๋Œ€ํ™”๊ฐ€ ์˜ค๊ฐ”์Šต๋‹ˆ๋‹ค.

์‹œ๊ฐํ™” ์˜ˆ์‹œ ํ™”๋ฉด

 

 

ํ˜„์žฌ ๊ตฌํ˜„๋œ ์ƒํƒœ์™€๋Š” ๋‹ค๋ฅด์ง€๋งŒ ์•„๋ž˜์™€๊ฐ™์€ ๊ณ ๋ฏผ์„ ํ–ˆ์—ˆ์–ด์š”.

 

"์•Œ๋ ˆ๋ฅด๊ธฐ ์ •๋„๋ฅผ ์–ด๋–ป๊ฒŒ ํ‘œํ˜„ํ•˜๋ฉด ์ข‹์„๊นŒ์š”?"

 

"์ƒ‰๊น”๋กœ ๊ตฌ๋ถ„ํ•˜๋ฉด ์–ด๋–จ๊นŒ์š”? ๋นจ๊ฐ„์ƒ‰์€ ์‹ฌํ•จ, ์ฃผํ™ฉ์ƒ‰์€ ๋ณดํ†ต, ์ดˆ๋ก์ƒ‰์€ ๋‚ฎ์Œ?"

 

"์ƒ‰์•ฝ์ด ์žˆ๋Š” ์‚ฌ์šฉ์ž๋„ ๊ณ ๋ คํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„์š”. ์ƒ‰๊น”๊ณผ ํ•จ๊ป˜ ํ…์ŠคํŠธ๋‚˜ ์•„์ด์ฝ˜๋„ ๋„ฃ์œผ๋ฉด ์–ด๋–จ๊นŒ์š”?"

 

์˜ˆ์‹œ ์ฝ”๋“œ๋Š” ์‹ค์ œ ๊ตฌํ˜„ ์ฝ”๋“œ์™€ ์ฐจ์ด๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

// ๋””์ž์ธ ํ† ํฐ ์ •์˜
const COLORS = {
  success: {
    main: '#00b896',
    background: '#e6f8f5',
    light: '#b3f0e6',
  },
  warning: {
    main: '#ffa726',
    background: '#fff3e0',
    light: '#ffd699',
  },
  error: {
    main: '#ff6b6b',
    background: '#ffebee',
    light: '#ffb3b3',
  },
  gray: {
    600: '#666666',
    500: '#999999',
    100: '#f5f5f5',
  },
} as const;

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ ˆ๋ฒจ ํƒ€์ž…
type AllergyLevel = 'low' | 'medium' | 'high';

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ์ดํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค
interface AllergyData {
  level: AllergyLevel;
  percentage: number;
  ingredients: string[];
  timestamp?: Date;
}

// ์•„์ด์ฝ˜ ์ปดํฌ๋„ŒํŠธ๋“ค (์‹ค์ œ๋กœ๋Š” ์•„์ด์ฝ˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์‚ฌ์šฉ)
const CheckCircleIcon = ({ size = 16 }: { size?: number }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
  </svg>
);

const AlertCircleIcon = ({ size = 16 }: { size?: number }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
  </svg>
);

const XCircleIcon = ({ size = 16 }: { size?: number }) => (
  <svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor">
    <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11H7v-2h10v2z"/>
  </svg>
);

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ ˆ๋ฒจ ์„ค์ •
interface AllergyLevelConfig {
  color: string;
  backgroundColor: string;
  text: string;
  icon: React.ReactNode;
  severity: number;
  description: string; // ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•œ ์„ค๋ช… ์ถ”๊ฐ€
}

const ALLERGY_LEVEL_CONFIG: Record<AllergyLevel, AllergyLevelConfig> = {
  low: {
    color: COLORS.success.main,
    backgroundColor: COLORS.success.background,
    text: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋‚ฎ์Œ',
    icon: <CheckCircleIcon size={16} />,
    severity: 1,
    description: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์ด ๋‚ฎ์€ ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค',
  },
  medium: {
    color: COLORS.warning.main,
    backgroundColor: COLORS.warning.background,
    text: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ณดํ†ต',
    icon: <AlertCircleIcon size={16} />,
    severity: 2,
    description: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์ด ๋ณดํ†ต ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค',
  },
  high: {
    color: COLORS.error.main,
    backgroundColor: COLORS.error.background,
    text: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋†’์Œ',
    icon: <XCircleIcon size={16} />,
    severity: 3,
    description: '์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ˜์‘์ด ๋†’์€ ์ˆ˜์ค€์ž…๋‹ˆ๋‹ค',
  },
} as const;

// Props ์ธํ„ฐํŽ˜์ด์Šค
interface AllergyItemProps {
  level: AllergyLevel;
  percentage: number;
  ingredients?: string[];
  showDetails?: boolean;
  className?: string;
  onToggleDetails?: () => void;
}

// ํผ์„ผํ‹ฐ์ง€ ํฌ๋งทํŒ… ํ•จ์ˆ˜
const formatPercentage = (value: number): string => {
  return `${Math.round(value)}%`;
};

// ๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ
export const AllergyItem: React.FC<AllergyItemProps> = ({
  level,
  percentage,
  ingredients = [],
  showDetails = false,
  className = '',
  onToggleDetails,
}) => {
  const config = ALLERGY_LEVEL_CONFIG[level];
  
  return (
    <div
      className={`allergy-item ${className}`}
      style={{
        backgroundColor: config.backgroundColor,
        border: `1px solid ${config.color}20`, // 20% ํˆฌ๋ช…๋„
        borderRadius: '12px',
        padding: '16px',
        marginBottom: '8px',
        transition: 'all 0.2s ease', // ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜
      }}
      role="article"
      aria-label={`${config.description}, ${formatPercentage(percentage)}`}
    >
      <div 
        className="allergy-header"
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          marginBottom: showDetails && ingredients.length > 0 ? '12px' : '0',
        }}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span 
            className="allergy-icon" 
            style={{ color: config.color, display: 'flex', alignItems: 'center' }}
            aria-hidden="true"
          >
            {config.icon}
          </span>
          <span 
            className="allergy-text" 
            style={{ 
              color: config.color, 
              fontWeight: '600',
              fontSize: '14px',
            }}
          >
            {config.text}
          </span>
        </div>
        
        <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
          <span 
            className="allergy-percentage"
            style={{
              fontWeight: '700',
              fontSize: '16px',
              color: config.color,
            }}
          >
            {formatPercentage(percentage)}
          </span>
          
          {ingredients.length > 0 && (
            <button
              onClick={onToggleDetails}
              style={{
                background: 'none',
                border: 'none',
                cursor: 'pointer',
                padding: '4px',
                borderRadius: '4px',
                color: config.color,
                fontSize: '12px',
                transition: 'background-color 0.2s ease',
              }}
              aria-expanded={showDetails}
              aria-label={showDetails ? '์„ธ๋ถ€์‚ฌํ•ญ ์ˆจ๊ธฐ๊ธฐ' : '์„ธ๋ถ€์‚ฌํ•ญ ๋ณด๊ธฐ'}
            >
              {showDetails ? 'โ–ฒ' : 'โ–ผ'}
            </button>
          )}
        </div>
      </div>

      {/* ์ƒ‰์•ฝ ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์ถ”๊ฐ€ ์‹œ๊ฐ์  ์š”์†Œ */}
      <div 
        style={{
          width: '100%',
          height: '4px',
          backgroundColor: `${config.color}30`,
          borderRadius: '2px',
          marginBottom: showDetails && ingredients.length > 0 ? '12px' : '0',
          overflow: 'hidden',
        }}
      >
        <div
          style={{
            width: `${percentage}%`,
            height: '100%',
            backgroundColor: config.color,
            borderRadius: '2px',
            transition: 'width 0.3s ease',
          }}
          role="progressbar"
          aria-valuenow={percentage}
          aria-valuemin={0}
          aria-valuemax={100}
          aria-label={`์•Œ๋ ˆ๋ฅด๊ธฐ ์ˆ˜์ค€ ${formatPercentage(percentage)}`}
        />
      </div>

      {showDetails && ingredients.length > 0 && (
        <div className="allergy-details">
          <h4 
            style={{ 
              margin: '0 0 8px 0',
              fontSize: '12px',
              color: COLORS.gray[600],
              fontWeight: '600',
            }}
          >
            ๊ด€๋ จ ์„ฑ๋ถ„ ({ingredients.length}๊ฐœ)
          </h4>
          <ul 
            className="allergy-ingredients"
            style={{
              margin: 0,
              padding: 0,
              listStyle: 'none',
              display: 'flex',
              flexWrap: 'wrap',
              gap: '6px',
            }}
          >
            {ingredients.map((ingredient, index) => (
              <li 
                key={`${ingredient}-${index}`}
                className="allergy-ingredient"
                style={{
                  backgroundColor: `${config.color}15`,
                  color: config.color,
                  padding: '4px 8px',
                  borderRadius: '12px',
                  fontSize: '12px',
                  fontWeight: '500',
                  border: `1px solid ${config.color}30`,
                }}
              >
                {ingredient}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  );
};

// ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ
interface AllergyListProps {
  allergies: AllergyData[];
  title?: string;
}

export const AllergyList: React.FC<AllergyListProps> = ({ 
  allergies, 
  title = "์•Œ๋ ˆ๋ฅด๊ธฐ ๋ถ„์„ ๊ฒฐ๊ณผ" 
}) => {
  const [expandedItems, setExpandedItems] = React.useState<Set<number>>(new Set());

  const toggleDetails = (index: number) => {
    setExpandedItems(prev => {
      const newSet = new Set(prev);
      if (newSet.has(index)) {
        newSet.delete(index);
      } else {
        newSet.add(index);
      }
      return newSet;
    });
  };

  // ๋ฐ์ดํ„ฐ ์ •๋ ฌ (์‹ฌ๊ฐ๋„ ์ˆœ)
  const sortedAllergies = React.useMemo(() => {
    return [...allergies].sort((a, b) => {
      const configA = ALLERGY_LEVEL_CONFIG[a.level];
      const configB = ALLERGY_LEVEL_CONFIG[b.level];
      return configB.severity - configA.severity; // ์‹ฌ๊ฐํ•œ ๊ฒƒ๋ถ€ํ„ฐ
    });
  }, [allergies]);

  if (allergies.length === 0) {
    return (
      <div style={{ 
        textAlign: 'center', 
        padding: '40px 20px',
        color: COLORS.gray[500],
      }}>
        ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
      </div>
    );
  }

  return (
    <div className="allergy-list" style={{ maxWidth: '600px', margin: '0 auto' }}>
      <h3 style={{ 
        fontSize: '18px', 
        fontWeight: '700', 
        marginBottom: '16px',
        color: COLORS.gray[600],
      }}>
        {title}
      </h3>
      
      {sortedAllergies.map((allergy, index) => (
        <AllergyItem
          key={index}
          level={allergy.level}
          percentage={allergy.percentage}
          ingredients={allergy.ingredients}
          showDetails={expandedItems.has(index)}
          onToggleDetails={() => toggleDetails(index)}
        />
      ))}
    </div>
  );
};

// ์‚ฌ์šฉ ์˜ˆ์‹œ
export const AllergyExample = () => {
  const sampleData: AllergyData[] = [
    {
      level: 'high',
      percentage: 85,
      ingredients: ['๋‹ญ๊ณ ๊ธฐ', '์Œ€', '์˜ฅ์ˆ˜์ˆ˜'],
    },
    {
      level: 'medium',
      percentage: 45,
      ingredients: ['์—ฐ์–ด', '๊ณ ๊ตฌ๋งˆ'],
    },
    {
      level: 'low',
      percentage: 15,
      ingredients: ['๋‹น๊ทผ', '๋ธŒ๋กœ์ฝœ๋ฆฌ', '์‚ฌ๊ณผ'],
    },
  ];

  return <AllergyList allergies={sampleData} title="์šฐ๋ฆฌ ๊ฐ•์•„์ง€ ์•Œ๋ ˆ๋ฅด๊ธฐ ๋ถ„์„" />;
};

 

"์›”๋ณ„ ํ†ต๊ณ„๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ ๊ทธ๋ž˜ํ”„๋กœ ํ‘œํ˜„ํ•˜๋ฉด ๋” ์ง๊ด€์ ์ผ ๊ฒƒ ๊ฐ™์•„์š”"๋ผ๋Š” ๋””์ž์ด๋„ˆ๋‹˜์˜ ์ œ์•ˆ์— ์–ด๋А์ •๋„ ํ™”๋ฉด์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

 

๋งˆ์น˜๋ฉฐ

์ง„ํ–‰ํ•˜๋ฉฐ ๊ณ ๋ฏผํ•œ ๋””ํ…Œ์ผ๋“ค์€ ํšŒ์‚ฌ์—์„  ์ด๋ฏธ ๋””์ž์ธ ์‹œ์Šคํ…œ์ด๋‚˜ ๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ์— ๋ฐ˜์˜๋˜์–ด ์žˆ์–ด์„œ, ๋ณ„๋‹ค๋ฅธ ๊ณ ๋ฏผ ์—†์ด ๊ฐ€์ ธ๋‹ค ์“ฐ๊ธฐ๋งŒ ํ–ˆ๋˜ ๊ฒƒ๋“ค์ด์—ˆ์–ด์š”.

์ด๋ฒˆ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๊ทธ๋Ÿฐ ์š”์†Œ๋“ค์„ ์ง์ ‘ ๊ธฐํšํ•˜๊ณ , ๋””์ž์ธ์— ๋…น์—ฌ๋‚ด๊ณ , ์ปดํฌ๋„ŒํŠธ๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ •์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๊ฒฝํ—˜ํ•ด๋ณด๋‹ˆ, ๊ฒ‰์œผ๋กœ๋งŒ ์•Œ๊ณ  ์žˆ์—ˆ๋˜ UI ๊ตฌ์„ฑ ์š”์†Œ๋“ค์ด ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ์˜๋„์™€ ๊ณ ๋ฏผ ์†์—์„œ ๋งŒ๋“ค์–ด์กŒ๋Š”์ง€๋ฅผ ์ƒˆ์‚ผ ๋А๋‚„ ์ˆ˜ ์žˆ์—ˆ์–ด์š”.

 

๋ฌด์—‡๋ณด๋‹ค ๊ทธ๋Ÿฐ ๊ณผ์ •์ด ์ƒ๊ฐ๋ณด๋‹ค ๊ฝค ์žฌ๋ฐŒ์—ˆ์Šต๋‹ˆ๋‹ค!

 

๊ธ€ ์ดˆ๋ฐ˜์— ์ด์•ผ๊ธฐํ–ˆ๋˜ ‘์ƒํƒœ’์— ๋Œ€ํ•œ ์ •์˜๋ฅผ ์Šคํ„ฐ๋””์›๋“ค๊ณผ ํ•จ๊ป˜ ๊ณ ๋ฏผํ•˜๊ณ  ์ฐพ์•„๋ณด๋˜ ์ค‘, ์•„๋ž˜์˜ ๋ธ”๋กœ๊ทธ ๊ธ€์„ ์ ‘ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ  ํฐ ๋„์›€์„ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ๐Ÿ‘‰ The Sorry State of States - velog.io/@tap_kim

 

์ €ํฌ๊ฐ€ ๊ฒช๊ณ  ์žˆ๋˜ ๊ณ ๋ฏผ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์„ ๋ณด๋ฉฐ ์ •๋ง ๋ฐ˜๊ฐ€์› ๊ณ ,
๋•๋ถ„์— ๋ง‰์—ฐํ–ˆ๋˜ ๋ฌธ์ œ๋“ค์„ ์กฐ๊ธˆ ๋” ๋น ๋ฅด๊ฒŒ ์ •๋ฆฌํ•˜๊ณ , ๋ช…ํ™•ํ•œ ๊ธฐ์ค€์„ ์„ธ์šธ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
(๋ฒˆ์—ญ)์ƒํƒœ๋“ค์˜ ์ฐธ๋‹ดํ•œ ์ƒํƒœ
๋กœ๊ทธ์ธ ๊ณผ์ •์˜ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€: ๊ฐœ์ธ์ •๋ณด ๋ณดํ˜ธ ๋ฐ ๋ณด์•ˆ

 

์˜ค๋Š˜์€ ์ œ๊ฐ€ Context API์—์„œ Zustand๋กœ ์ƒํƒœ๊ด€๋ฆฌ ๋„๊ตฌ๋ฅผ ์ „ํ™˜ํ•˜๊ฒŒ ๋œ ๊ณผ์ •๊ณผ 2024๋…„ ํ˜„์žฌ ๋ฆฌ์•กํŠธ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์˜ ํ˜„ํ™ฉ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

 

1. Context API์˜ ํ•œ๊ณ„์™€ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ํ•„์š”์„ฑ

Context API๋กœ ์‹œ์ž‘ํ•œ ์ด์œ 

์ฒ˜์Œ ์•Œ๋ฆฌ๋‹ˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•  ๋•Œ๋Š” Context API๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ์ธ์ฆ, ํ…Œ๋งˆ, ์–ธ์–ด ์„ค์ • ๋“ฑ ์ „์—ญ์ ์œผ๋กœ ๊ณต์œ ๋˜์–ด์•ผ ํ•˜๋Š” ์ƒํƒœ๋“ค์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ Context API ๋งŒ์œผ๋กœ ์ถฉ๋ถ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

// ์ดˆ๊ธฐ Context API ์‚ฌ์šฉ ์˜ˆ์‹œ
const PetContext = React.createContext<PetContextType | undefined>(undefined);

export const PetProvider: React.FC<PropsWithChildren> = ({ children }) => {
  const [petData, setPetData] = useState<PetData>({
    allergies: [],
    symptoms: [],
    foodLog: []
  });

  return (
    <PetContext.Provider value={{ petData, setPetData }}>
      {children}
    </PetContext.Provider>
  );
};

 

ํ•œ๊ณ„์  ๋ฐœ๊ฒฌ

ํ•˜์ง€๋งŒ ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉด์„œ ์—ฌ๋Ÿฌ ๊ฐ€์ง€ ํ•œ๊ณ„์ ์ด ๋“œ๋Ÿฌ๋‚˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค:

 

  • ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง: Context์˜ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ํ•ด๋‹น Context๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋˜์—ˆ์Šต๋‹ˆ๋‹ค. 
  • ์ฝ”๋“œ ๋ณต์žก๋„ ์ฆ๊ฐ€: ์—ฌ๋Ÿฌ ๊ฐœ์˜ Context๋ฅผ ์ค‘์ฒฉํ•ด์„œ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋‹ˆ Provider Hell์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.
  • ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง ๊ด€๋ฆฌ์˜ ์–ด๋ ค์›€: ๋น„๋™๊ธฐ ์ž‘์—…๊ณผ ๋ณต์žกํ•œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐ ํ•œ๊ณ„๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

2. ์ฃผ์š” ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋น„๊ต ๋ถ„์„

Redux

Redux๋Š” ์˜ค๋žซ๋™์•ˆ ๋ฆฌ์•กํŠธ ์ƒํƒœ๊ณ„์˜ ํ‘œ์ค€๊ณผ ๊ฐ™์€ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ž…๋‹ˆ๋‹ค.

Redux์˜ ํ•ต์‹ฌ ๊ฐœ๋…

 

  • Action: ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์œ„ํ•œ ์ด๋ฒคํŠธ๋ฅผ ์„ค๋ช…ํ•˜๋Š” ๊ฐ์ฒด
  • Reducer: Action์„ ๋ฐ›์•„ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์ˆœ์ˆ˜ ํ•จ์ˆ˜
  • Store: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ๊ฐ์ฒด

Redux ๋ฏธ๋“ค์›จ์–ด ์ƒํƒœ๊ณ„

 

  • Redux Thunk: ๋น„๋™๊ธฐ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ๋ฏธ๋“ค์›จ์–ด
  • Redux Saga: ๋ณต์žกํ•œ ๋น„๋™๊ธฐ ํ๋ฆ„์„ ์ œ์–ดํ•˜๊ธฐ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด
    • Generator ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ ์„ ์–ธ์  ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ
    • ๋ณต์žกํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค(API ์š”์ฒญ ์ทจ์†Œ, ์žฌ์‹œ๋„ ๋“ฑ) ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
  • Redux Toolkit: ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๋ฅผ ์ค„์ด๊ธฐ ์œ„ํ•œ ๊ณต์‹ ๋„๊ตฌ

๊ทธ๋Ÿฌ๋‚˜ Redux๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

 

  • ๋งŽ์€ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ
  • ํ•™์Šต ๊ณก์„ ์ด ๊ฐ€ํŒŒ๋ฆ„
  • ์ž‘์€ ์ƒํƒœ ๋ณ€ํ™”์—๋„ ๋งŽ์€ ์ฝ”๋“œ ํ•„์š”

Recoil

Facebook์—์„œ ๋งŒ๋“  ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

 

์žฅ์ 

  • React์™€์˜ ๋†’์€ ํ˜ธํ™˜์„ฑ
  • ๊ฐ„๋‹จํ•œ API (atom, selector)
  • ๋น„๋™๊ธฐ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๊ฐ€ ์‰ฌ์›€

๋‹จ์ 

  • ์•„ํ†ฐ์œผ๋กœ ๊ด€๋ฆฌ๋˜๋Š” ์ „์—ญ ์ƒํƒœ๋ฅผ ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๋ผ๋„ ๋ฐ”๋กœ ์•„ํ†ฐ์„ ๊ตฌ๋…ํ•ด์„œ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›๊ธฐ์—, ์˜์กด์„ฑ์ด ์—ฌ๋Ÿฌ ๋ฐฉํ–ฅ์œผ๋กœ ์—ฎ์ด๋ฉด์„œ ์˜ˆ์ธกํ•˜๊ธฐ ํž˜๋“ค์–ด์ง‘๋‹ˆ๋‹ค.

Zustand

๊ฐ€๋ณ๊ณ  ์ง๊ด€์ ์ธ API๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ˜„๋Œ€์ ์ธ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์ž…๋‹ˆ๋‹ค.

 

์ฃผ์š” ํŠน์ง•

  • ์ตœ์†Œํ•œ์˜ ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ
  • TypeScript ์ง€์›์ด ํ›Œ๋ฅญํ•จ
  • Redux DevTools ์ง€์›
  • ๋ฏธ๋“ค์›จ์–ด ์‹œ์Šคํ…œ (persist, devtools ๋“ฑ)

์˜ˆ์‹œ ์ฝ”๋“œ:

import create from 'zustand'

interface PetStore {
  symptoms: Symptom[]
  addSymptom: (symptom: Symptom) => void
  foodLogs: FoodLog[]
  addFoodLog: (log: FoodLog) => void
}

const usePetStore = create<PetStore>((set) => ({
  symptoms: [],
  addSymptom: (symptom) => set((state) => ({
    symptoms: [...state.symptoms, symptom]
  })),
  foodLogs: [],
  addFoodLog: (log) => set((state) => ({
    foodLogs: [...state.foodLogs, log]
  }))
}))

 

3. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ์˜ ์„ ํƒ: Zustand

์•Œ๋ฆฌ๋‹ˆ ํ”„๋กœ์ ํŠธ์—์„œ์˜ ์š”๊ตฌ์‚ฌํ•ญ

  1. ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ
  2. ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ (์•Œ๋ ˆ๋ฅด๊ธฐ ์ฆ์ƒ, ์Œ์‹ ๊ธฐ๋ก, ๋ถ„์„ ๋ฐ์ดํ„ฐ)
  3. ์„ฑ๋Šฅ ์ตœ์ ํ™”
  4. TypeScript ์ง€์›

Zustand ์„ ํƒ ์ด์œ 

  1. ๊ฐ„๋‹จํ•œ ์„ค์ •๊ณผ ์‚ฌ์šฉ๋ฒ•: ๋ณ„๋„์˜ Provider ์„ค์ •์ด ํ•„์š” ์—†๊ณ , ์ง๊ด€์ ์ธ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  2. ์„ฑ๋Šฅ ์ตœ์ ํ™”: ํ•„์š”ํ•œ ์ƒํƒœ๋งŒ ๊ตฌ๋…ํ•˜์—ฌ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
  3. TypeScript ์ง€์›: ํƒ€์ž… ์ถ”๋ก ์ด ์ž˜ ๋™์ž‘ํ•˜์—ฌ ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ์ด ํ–ฅ์ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
  4. ๋ฏธ๋“ค์›จ์–ด ์ง€์›: persist ๋ฏธ๋“ค์›จ์–ด๋ฅผ ํ†ตํ•ด ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์—ฐ๋™์ด ์‰ฌ์› ์Šต๋‹ˆ๋‹ค.

Context API์™€์˜ ์กฐํ™”

Context API์™€ Zustand๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์—ญํ• ์„ ๊ตฌ๋ถ„ํ–ˆ์Šต๋‹ˆ๋‹ค:

  • Context API: ํ…Œ๋งˆ, ์ธ์ฆ ๋“ฑ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „๋ฐ˜์ ์ธ ์„ค์ •๊ณผ ๊ด€๋ จ๋œ ์ƒํƒœ ๊ด€๋ฆฌ (๋‹จ์–ด ๊ทธ๋Œ€๋กœ ํŠน์ • ๋งฅ๋ฝ์„ ์”Œ์šธ๋•Œ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.)
  • Zustand: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ด€๋ จ๋œ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ

 

4. ๊ฒฐ๋ก 

2024๋…„ ํ˜„์žฌ, ์ €๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๊ธฐ์ค€์œผ๋กœ ์ƒํƒœ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. ์ž‘์€ ํ”„๋กœ์ ํŠธ: Context API๋งŒ์œผ๋กœ๋„ ์ถฉ๋ถ„
  2. ์ค‘๊ฐ„ ๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ: Zustand
  3. ๋Œ€๊ทœ๋ชจ ํ”„๋กœ์ ํŠธ: Redux Toolkit ๋˜๋Š” Zustand

์•Œ๋ฆฌ๋‹ˆ์™€ ๊ฐ™์ด ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์™€ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ ํ”„๋กœ์ ํŠธ์—๋Š” Zustand๊ฐ€ ํŠนํžˆ ์ ํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ๊ฐ€ ๊ฐ„๊ฒฐํ•˜๋ฉด์„œ๋„ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฉฐ, ํŠนํžˆ TypeScript์™€์˜ ํ˜ธํ™˜์„ฑ์ด ๋›ฐ์–ด๋‚˜ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์ด ๋งค์šฐ ์ข‹์•˜์Šต๋‹ˆ๋‹ค.

 

 

 

 

ํ”„๋ฆฌํ”ฝ์Šค!!

 

ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์„ ํ•˜๋‹ค ๋ณด๋ฉด ํฌ๋กœ์Šค๋ธŒ๋ผ์šฐ์ง• ์ด์Šˆ๋Š” ํ”ผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

::-webkit-input-placeholder์™€ ๊ฐ™์€ ๋ธŒ๋ผ์šฐ์ €๋ณ„ ํ”„๋ฆฌํ”ฝ์Šค(-webkit-, -moz-, -ms- ๋“ฑ)๋ฅผ ์ผ์ผ์ด ์ถ”๊ฐ€ํ•˜๋Š” ๊ฑด ๊ต‰์žฅํžˆ ๋ฒˆ๊ฑฐ๋กœ์šด ์ž‘์—…์ด์˜ˆ์š”.

 

์‚ฌ์‹ค ํ˜„์—…์—์„œ๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ์ด๋Ÿฌํ•œ ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ด ๋งค์šฐ ๊ณค๋ž€ํ•œ ์ผ์ž…๋‹ˆ๋‹ค.

(์ €๋„ ๊ด€๋ จ๋œ ๋Œ€ํ™”๋ฅผ ๋‚˜๋ˆ„๋‹ค ์•Œ๊ฒŒ๋์–ด์š”!)

 

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฒˆ๋“ค๋Ÿฌ๋ฅผ ํ†ตํ•ด ์ž๋™์œผ๋กœ ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•์ธ Autoprefixer๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

PostCSS๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ผ๋ฉด, ํ˜น์€ CSS๋กœ ๋ณ€ํ™˜ํ•œ ํ›„ postcss-loader๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ฒŒ ํ•œ๋‹ค๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ฐ ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์Šคํƒ€์ผ์„ ๋”ฐ๋กœ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„๋„ Autoprefixer๊ฐ€ ์›นํ‚ท ํ”„๋ฆฌํ”ฝ์Šค์™€ ๊ฐ™์€ ๊ฒƒ์„ ์ž๋™์œผ๋กœ ๋ถ™์—ฌ์ค๋‹ˆ๋‹ค.

 

์ด๋ฒˆ ํฌ์ŠคํŠธ์—์„œ๋Š” PostCSS์™€ Autoprefixer๋ฅผ ํ™œ์šฉํ•ด ํ•ด๋‹น ISSUE๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‹ค์ œ ํ”„๋กœ์ ํŠธ ๊ฒฝํ—˜์„ ๋ฐ”ํƒ•์œผ๋กœ ์ƒ์„ธํžˆ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค!

 

PostCSS์™€ Autoprefixer๋ž€?

PostCSS

  • JavaScript ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•ด CSS๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ๋„๊ตฌ
  • "Post-processor"๋ผ๊ณ ๋„ ๋ถˆ๋ฆฌ๋Š” ์ด์œ ๋Š” CSS๋ฅผ ์ž‘์„ฑํ•œ ํ›„์— ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ
  • CSS ๊ตฌ๋ฌธ ๋ถ„์„์— ์ตœ์ ํ™”๋œ ํŒŒ์„œ๋ฅผ ์‚ฌ์šฉํ•ด ๋†’์€ ์„ฑ๋Šฅ์„ ๋ณด์ž„
  • CSS ์ „์ฒ˜๋ฆฌ๊ธฐ(Sass)๋‚˜ ํ›„์ฒ˜๋ฆฌ๊ธฐ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (์ด ๋ถ€๋ถ„์€ ์ œ๊ฐ€ ํ—ท๊ฐˆ๋ ธ๋˜ ๋ถ€๋ถ„์ด๋ผ ์•„๋ž˜ ์ถ”๊ฐ€์„ค๋ช…์„ ๋„ฃ์—ˆ์–ด์š”!)
    1. CSS ์ „์ฒ˜๋ฆฌ๊ธฐ(SASS):
      CSS ๊ธฐ๋Šฅ์„ ํ™•์žฅํ•œ ์–ธ์–ด ์ž…๋‹ˆ๋‹ค. ๋ณ€์ˆ˜, ์ค‘์ฒฉ ๊ทœ์น™, ๋ฏน์Šค์ธ ๋“ฑ์„ ์ง€์›ํ•˜๊ณ , PostCSS๋Š” Sass๋กœ ์ž‘์„ฑํ•œ ์ฝ”๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, PostCSS ์ž์ฒด๋Š” Sass ๋ฌธ๋ฒ•์„ ์‚ฌ์šฉํ•˜์ง„ ์•Š์•„์š”.
    2. CSS ํ›„์ฒ˜๋ฆฌ๊ธฐ:
      PostCSS๋Š” CSS์ฝ”๋“œ๊ฐ€ ์ž‘์„ฑ๋œ ํ›„์— ์ถ”๊ฐ€์ ์ธ ๋ณ€ํ™˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋„๊ตฌ๋กœ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
      ์˜ˆ๋ฅผ ๋“ค์–ด, ์ œ๊ฐ€ ์ง€๊ธˆ ์ž‘์„ฑํ•˜๋Š” Autoprefixer ๊ฐ™์€ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•ด์„œ ์ž๋™์œผ๋กœ ๋ธŒ๋ผ์šฐ์ € ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ์š”!
  • 200๊ฐœ ์ด์ƒ์˜ ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ ์ œ๊ณต (์ƒํƒœ๊ณ„๊ฐ€ ํฝ๋‹ˆ๋‹ค!)
  • ๋ชจ๋“ˆํ™”๋œ ๊ตฌ์กฐ๋กœ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋งŒ ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ์„ ํƒ์  ์ถ”๊ฐ€ ๊ฐ€๋Šฅ

Autoprefixer

  • PostCSS์˜ ๋Œ€ํ‘œ์ ์ธ ํ”Œ๋Ÿฌ๊ทธ์ธ
  • ๋ธŒ๋ผ์šฐ์ € ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋„๊ตฌ
  • Browserslist ์„ค์ •์„ ํ†ตํ•ด ์ง€์› ๋ธŒ๋ผ์šฐ์ € ๋ฒ”์œ„ ์ง€์ • ๊ฐ€๋Šฅ

 

Create React App๊ณผ Vite์˜ ๊ธฐ๋ณธ ์ง€์›

ํ•ด๋‹น ๋ถ€๋ถ„์€ Webpack์„ ์ง์ ‘ ์„ค์ •ํ•˜์ง€ ์•Š์„ ๋• ์–ด๋–ป๊ฒŒ ๋˜๋Š” ๊ฑด์ง€, ๊ธฐ๋ณธ ์„ค์ •์ด ๋˜์–ด ์žˆ๋Š”์ง€ ๋“ฑ์ด ๊ถ๊ธˆํ•ด ์ฐพ์•„๋ณด๊ฒŒ ๋์Šต๋‹ˆ๋‹ค.

Create React App (CRA)

  • PostCSS์™€ Autoprefixer๊ฐ€ ๊ธฐ๋ณธ์œผ๋กœ ์„ค์ •๋˜์–ด ์žˆ์Œ
  • ๋ณ„๋„ ์„ค์ • ์—†์ด ๋ฐ”๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
  • browserslist ์„ค์ •๋„ ๊ธฐ๋ณธ ์ œ๊ณต

CRA์˜ package.json์„ ๋ณด์‹œ๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ํ™•์ธํ•˜์‹ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

"browserslist": {
  "production": [
    ">0.2%",
    "not dead",
    "not op_mini all"
  ],
  "development": [
    "last 1 chrome version",
    "last 1 firefox version",
    "last 1 safari version"
  ]
}


Vite

  • PostCSS๋Š” ๊ธฐ๋ณธ ์ง€์›
  • Autoprefixer๋Š” ๋ณ„๋„ ์„ค์น˜ ํ•„์š”
yarn add -D autoprefixer
// vite.config.js
import autoprefixer from 'autoprefixer'

export default {
  css: {
    postcss: {
      plugins: [
        autoprefixer()
      ]
    }
  }
}

 

ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์— Custom Webpack ์„ค์ •ํ•˜๊ธฐ

์ปค์Šคํ…€ Webpack ์„ค์ •์—์„œ๋Š” ์ˆ˜๋™์œผ๋กœ PostCSS์™€ Autoprefixer๋ฅผ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ํ•„์š”ํ•œ ํŒจํ‚ค์ง€ ์„ค์น˜

yarn add -D postcss postcss-loader autoprefixer


์›นํŒฉ ์„ค์ • ์˜ˆ์‹œ

module.exports = {
  module: {
    rules: [
      {
        test: /\.module\.scss$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              modules: {
                localIdentName: "[name]__[local]--[hash:base64:5]",
              },
              importLoaders: 2,
            },
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  ["autoprefixer"]
                ],
              },
            },
          },
          "sass-loader",
        ],
      },
      {
        test: /\.(scss|css)$/,
        exclude: /\.module\.scss$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
            },
          },
          {
            loader: "postcss-loader",
            options: {
              postcssOptions: {
                plugins: [
                  ["autoprefixer"]
                ],
              },
            },
          },
          "sass-loader",
        ],
      },
    ],
  },
  // ... ๊ธฐํƒ€ ์›นํŒฉ ์„ค์ •
};

 

์ถ”๊ฐ€์ ์œผ๋กœ ์ €๋Š” ์„ค์ •ํ•˜์ง€ ์•Š์•˜์ง€๋งŒ ๋‹ค๋ฅธ ๋ธ”๋กœ๊ทธ ๊ธ€์„ ๋ณด์‹œ๋ฉด ๊ฐ„ํ˜น postcss-scss์— ๋Œ€ํ•œ ์ถ”๊ฐ€๊ฐ€ ๋ณด์ด์…จ์„ ํ…๋ฐ, ์ด๊ฑด ๋ฌด์Šจ ์—ญํ• ์ผ๊นŒ์š”?!

postcss-scss๋ž€?

  • ์ผ๋ฐ˜์ ์œผ๋กœ ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
  • sass-loader๊ฐ€ SCSS๋ฅผ CSS๋กœ ๋ณ€ํ™˜ํ•œ ํ›„ postcss-loader๊ฐ€ ์ฒ˜๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ
  • postcss-scss๋Š” SCSS ๊ตฌ๋ฌธ์„ ์ง์ ‘ PostCSS ํ”Œ๋Ÿฌ๊ทธ์ธ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ๋งŒ ํ•„์š”

 

์‹ค์ˆ˜ํ–ˆ๋˜ ๋ถ€๋ถ„๊ณผ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

package.json์— ์ž‘์„ฑ๋ผ์•ผ ํ•˜๋Š” json ๋ฌธ๋ฒ•์„. browserslistrc ํŒŒ์ผ์— ์ ์šฉํ•ด ๋ฒ„๋ ธ์–ด์š”. ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฝ๋‹ค ์ˆœ๊ฐ„์ ์œผ๋กœ ์ฐฉ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋„ˆ๋ฌด ๊ฐ„๋‹จํ•˜์ง€๋งŒ ์ œ ๊ธฐ์ค€์—์„œ ์น˜๋ช…์ ์ธ ์‹ค์ˆ˜๋ž€ ์ƒ๊ฐ์ด ๋“ค์–ด ๊ธฐ๋กํ•ด๋‘๋ ค ํ•ฉ๋‹ˆ๋‹ค!

Browserslist ์„ค์ • ์˜ค๋ฅ˜

Error [BrowserslistError]: Unknown browser query `{`. Maybe you are using old Browserslist or made typo in query.

 

 

.browserslistrc ํŒŒ์ผ ์‚ฌ์šฉ (๊ถŒ์žฅ)

์ €๋Š” ์ตœ์ข…์ ์œผ๋กœ ์•„๋ž˜์˜ ์ฝ”๋“œ๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค!

# ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ
[production]
>0.2% # ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ๋ฅ  0.2% ์ด์ƒ์ธ ๋ธŒ๋ผ์šฐ์ €
not dead # ๊ณต์‹ ์ง€์›์ด ์ค‘๋‹จ๋˜์ง€ ์•Š์€ ๋ธŒ๋ผ์šฐ์ €
not op_mini all # Opera Mini ๋ธŒ๋ผ์šฐ์ € ์ œ์™ธ

# ๊ฐœ๋ฐœ ํ™˜๊ฒฝ
[development]
last 1 chrome version # ์ตœ์‹  ํฌ๋กฌ ๋ฒ„์ „
last 1 firefox version # ์ตœ์‹  ํŒŒ์ด์–ดํญ์Šค ๋ฒ„์ „
last 1 safari version # ์ตœ์‹  ์‚ฌํŒŒ๋ฆฌ ๋ฒ„์ „


ํ•ด๊ฒฐ๋ฐฉ๋ฒ• 2: package.json์— ์„ค์ •

{
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}


Browserslist ์„ค์ • ๊ฐ€์ด๋“œ

์ฃผ์š” ์ฟผ๋ฆฌ ์„ค๋ช…

  • >0.2%: ์ „ ์„ธ๊ณ„ ์‚ฌ์šฉ๋ฅ  0.2% ์ด์ƒ์ธ ๋ธŒ๋ผ์šฐ์ €
  • not dead: ๊ณต์‹ ์ง€์›์ด ์ค‘๋‹จ๋˜์ง€ ์•Š์€ ๋ธŒ๋ผ์šฐ์ €
  • not op_mini all: Opera Mini ๋ธŒ๋ผ์šฐ์ € ์ œ์™ธ
  • last 1 chrome version: ์ตœ์‹  ํฌ๋กฌ ๋ฒ„์ „
  • last 2 versions: ๊ฐ ๋ธŒ๋ผ์šฐ์ €์˜ ์ตœ์‹  2๊ฐœ ๋ฒ„์ „
  • > 1%: ์ „ ์„ธ๊ณ„ ์ ์œ ์œจ 1% ์ด์ƒ
  • IE 11: IE 11 ์ง€์›

์„ค์ • ํ…Œ์ŠคํŠธ

  • browserslist.dev์—์„œ ์„ค์ •์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Œ
  • ์–ด๋–ค ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํฌํ•จ๋˜๋Š”์ง€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธ ๊ฐ€๋Šฅ

 

์‹ค์ œ ๋™์ž‘ ์˜ˆ์‹œ

๋ณ€ํ™˜ ์ „ CSS

.example {
  display: flex;
  user-select: none;
}

๋ณ€ํ™˜ ํ›„ CSS

.example {
  display: -webkit-box;
  display: -ms-flexbox;
  display: flex;
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
}

 

์ฃผ์˜์‚ฌํ•ญ๊ณผ ํŒ!

  • ๋กœ๋” ์ˆœ์„œ: PostCSS ๋กœ๋”๋Š” Sass ๋กœ๋” ์ดํ›„, CSS ๋กœ๋” ์ด์ „์— ์œ„์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.  
    1. sass-loader (SCSS -> CSS ๋ณ€ํ™˜)
    2. postcss-loader (์ž๋™ ํ”„๋ฆฌํ”ฝ์Šค ์ถ”๊ฐ€)
    3. css-loader (CSS -> JS ๋ณ€ํ™˜)
    4. style-loader (JS -> style ํƒœ๊ทธ ์‚ฝ์ž…)
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”: development ํ™˜๊ฒฝ์—์„œ๋Š” source map์„ ํ™œ์„ฑํ™”ํ•˜๊ณ , production์—์„œ๋Š” ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„: browserslist ์„ค์ •์„ ํ†ตํ•ด ์ง€์›ํ•  ๋ธŒ๋ผ์šฐ์ € ๋ฒ”์œ„๋ฅผ ์ ์ ˆํžˆ ์กฐ์ ˆํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ฃผ์˜์‚ฌํ•ญ๊ณผ ํŒ์—์„œ์˜ Development ํ™˜๊ฒฝ์—์„œ source map ํ™œ์„ฑํ™”ํ•˜๋Š” ์ด์œ  & Production ํ™˜๊ฒฝ์—์„œ ๋น„ํ™œ์„ฑํ™” ํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด์„œ๋งŒ ์ข€ ๋” ์ž์„ธํžˆ ์•Œ์•„๋ณผ๊ฒŒ์š”!

 

Development ํ™˜๊ฒฝ์—์„œ source map ํ™œ์„ฑํ™”ํ•˜๋Š” ์ด์œ :

  1. ๋””๋ฒ„๊น… ์šฉ์ด์„ฑ
    • ๊ฐœ๋ฐœ ์ค‘์—๋Š” ์›๋ณธ ์†Œ์Šค ์ฝ”๋“œ์™€ ๋ณ€ํ™˜๋œ ์ฝ”๋“œ๋ฅผ ๋งคํ•‘ํ•˜์—ฌ ์‰ฝ๊ฒŒ ๋””๋ฒ„๊น…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    • ๋ธŒ๋ผ์šฐ์ €์˜ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์‹ค์ œ ์ž‘์„ฑํ•œ CSS ํŒŒ์ผ์˜ ์œ„์น˜์™€ ๋ผ์ธ์„ ์ •ํ™•ํžˆ ํ™•์ธ ๊ฐ€๋Šฅ
    • PostCSS๋กœ ๋ณ€ํ™˜๋œ ์ฝ”๋“œ๊ฐ€ ์•„๋‹Œ ์›๋ณธ ์ฝ”๋“œ๋ฅผ ์ง์ ‘ ํ™•์ธํ•˜๋ฉฐ ์ˆ˜์ • ๊ฐ€๋Šฅ
  2. ๊ฐœ๋ฐœ ํšจ์œจ์„ฑ
    • ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์ฆ‰๊ฐ์ ์ธ ํ”ผ๋“œ๋ฐฑ ํ™•์ธ ๊ฐ€๋Šฅ
    • CSS ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ์ •ํ™•ํ•œ ์œ„์น˜ ์ถ”์ ์ด ๊ฐ€๋Šฅํ•˜์—ฌ ์ˆ˜์ • ์‹œ๊ฐ„ ๋‹จ์ถ•
    • ๋ณต์žกํ•œ ์Šคํƒ€์ผ ๊ตฌ์กฐ์—์„œ๋„ ์›๋ณธ ์ฝ”๋“œ ์œ„์น˜๋ฅผ ์‰ฝ๊ฒŒ ํŒŒ์•…!

Production ํ™˜๊ฒฝ์—์„œ source map ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ์ด์œ :

  1. ํŒŒ์ผ ํฌ๊ธฐ ์ตœ์ ํ™”
    • source map ํŒŒ์ผ์€ ์›๋ณธ ์ฝ”๋“œ์™€ ๋ณ€ํ™˜๋œ ์ฝ”๋“œ์˜ ๋งคํ•‘ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋ฏ€๋กœ ์ƒ๋‹นํ•œ ํฌ๊ธฐ๋ฅผ ์ฐจ์ง€
    • ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ถˆํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์ „์†ก์„ ์ค„์—ฌ ๋กœ๋”ฉ ์†๋„ ํ–ฅ์ƒ
    • ์ผ๋ฐ˜์ ์œผ๋กœ source map ํŒŒ์ผ์€ ์›๋ณธ ํŒŒ์ผ ํฌ๊ธฐ์˜ ์ ˆ๋ฐ˜ ์ด์ƒ์„ ์ฐจ์ง€ํ•  ์ˆ˜ ์žˆ์Œ
  2. ๋ณด์•ˆ ๊ฐ•ํ™”
    • source map์ด ๋…ธ์ถœ๋˜๋ฉด ์›๋ณธ ์ฝ”๋“œ ๊ตฌ์กฐ๊ฐ€ ๋“œ๋Ÿฌ๋‚  ์ˆ˜ ์žˆ์Œ
    • ์•…์˜์ ๋ฅผ ๊ฐ€์ง„ ์‚ฌ์šฉ์ž๊ฐ€ ์ฝ”๋“œ ๊ตฌ์กฐ๋ฅผ ๋ถ„์„ํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€
    • ๊ธฐ์—…์˜ proprietary ์ฝ”๋“œ ๋ณดํ˜ธ!!
  3. ์„ฑ๋Šฅ ์ตœ์ ํ™”
    • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ source map์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ถ”๊ฐ€์ ์ธ ์ž‘์—… ์ œ๊ฑฐ
    • ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์‹œ๊ฐ„ ๋‹จ์ถ•
    • ์„œ๋ฒ„ ๋Œ€์—ญํญ ์‚ฌ์šฉ๋Ÿ‰ ๊ฐ์†Œ

์‹ค์ œ ์„ค์ • ์˜ˆ์‹œ! (ํ”„๋กœ์ ํŠธ ์ฝ”๋“œ๋ณด๋‹ค ๊ฐ„๋žตํ™”)

// webpack.config.js
module.exports = {
  mode: process.env.NODE_ENV,
  module: {
    rules: [{
      test: /\.css$/,
      use: [
        'style-loader',
        {
          loader: 'css-loader',
          options: {
            sourceMap: process.env.NODE_ENV === 'development'
          }
        },
        {
          loader: 'postcss-loader',
          options: {
            sourceMap: process.env.NODE_ENV === 'development'
          }
        }
      ]
    }]
  }
};

 

 

๊ฒฐ๋ก 

PostCSS์™€ Autoprefixer๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํฌ๋กœ์Šค๋ธŒ๋ผ์šฐ์ง•์„ ์œ„ํ•œ ์ˆ˜๋™ ์ž‘์—…์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

CRA๋‚˜ Create Vite๊ฐ™์€ ๊ฒฝ์šฐ ์ด๋ฏธ ์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ๊ธฐ๋ณธ์œผ๋กœ ์ œ๊ณตํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ปค์Šคํ…€ ์›นํŒฉ ์„ค์ •์—์„œ๋„ ๊ฐ„๋‹จํ•œ ์„ค์ •๋งŒ์œผ๋กœ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

ํŠนํžˆ Browserslist ์„ค์ •์„ ํ†ตํ•ด ํ”„๋กœ์ ํŠธ์˜ ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ , ํ•„์š”ํ•œ ํ”„๋ฆฌํ”ฝ์Šค๋งŒ ์ถ”๊ฐ€ํ•จ์œผ๋กœ์จ ์ตœ์ ํ™”๋œ CSS๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒ ์ฃ ?!

 

.browserslistrc ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๋ฉด ์„ค์ • ๊ด€๋ฆฌ๊ฐ€ ๋”์šฑ ์šฉ์ดํ•ด์ง€๋ฉฐ, ๋‹ค๋ฅธ ๋„๊ตฌ๋“ค๊ณผ๋„ ์„ค์ •์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์—ฌ๊ธฐ์„œ "๋‹ค๋ฅธ ๋„๊ตฌ๋“ค"์€ ์ฃผ๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์€ CSS ๋˜๋Š” JavaScript ๋„๊ตฌ๋“ค์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค!

  1. Autoprefixer: CSS์— ํ•„์š”ํ•œ ๋ธŒ๋ผ์šฐ์ € ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋„๊ตฌ๋กœ, Browserslist ์„ค์ •์„ ํ†ตํ•ด ์–ด๋–ค ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์ถ”๊ฐ€ํ• ์ง€ ๊ฒฐ์ • (์ง€๊ธˆ๊นŒ์ง€ ์„ค๋ช…๋“œ๋ฆฐ ๋‚ด์šฉ์ด์ฃ ?!)
  2. Babel: ์ตœ์‹  JavaScript ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜• ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์ž‘๋™ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋ณ€ํ™˜ํ•ด์ฃผ๋Š” ๋„๊ตฌ๋กœ, Browserslist ์„ค์ •์„ ํ†ตํ•ด ์ง€์›ํ•  ๋ธŒ๋ผ์šฐ์ € ์ •์˜ ๊ฐ€๋Šฅ
  3. ESLint: JavaScript ์ฝ”๋“œ์˜ ํ’ˆ์งˆ์„ ๊ด€๋ฆฌํ•˜๋Š” ๋„๊ตฌ๋กœ, Browserslist์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด ํŠน์ • ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์— ๋งž๋Š” ์ฝ”๋“œ ์Šคํƒ€์ผ ์ ์šฉ ๊ฐ€๋Šฅ
  4. Stylelint: CSS ์Šคํƒ€์ผ์„ ๊ฒ€์‚ฌํ•˜๋Š” ๋„๊ตฌ๋กœ, Browserslist ์„ค์ •์„ ์ฐธ๊ณ ํ•˜์—ฌ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ ๊ด€๋ จ ๊ทœ์น™ ์ ์šฉ ๊ฐ€๋Šฅ

์ฆ‰, .browserslistrc ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๋ฉด ์ด๋Ÿฌํ•œ ๋‹ค์–‘ํ•œ ๋„๊ตฌ๋“ค์ด ๋™์ผํ•œ ๋ธŒ๋ผ์šฐ์ € ์ง€์› ๋ฒ”์œ„๋ฅผ ๊ณต์œ ํ•˜๊ฒŒ ๋˜์–ด, ๊ฐœ๋ฐœ ๋ฐ ์œ ์ง€๋ณด์ˆ˜ ๊ณผ์ •์—์„œ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
PostCSS ๊ณต์‹ ๋ฌธ์„œ
Autoprefixer GitHub
Browserslist
Create React App PostCSS ์„ค์ •
Vite CSS ์„ค์ •
Webpack 5 postcss-loader 
Browserslist ๊ด€๋ จ ๊ฐœ์ธ ๋ธ”๋กœ๊ทธ

 

express-openapi-validator

 

์•ˆ๋…•ํ•˜์„ธ์š”! express-openapi-validator์˜ HTTP ์ธ์ฆ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด PR์„ ์ œ์ถœํ–ˆ๋˜ ๊ณผ์ •์„ ๊ณต์œ ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ด์ „ ํฌ์ŠคํŠธ์—์„œ express-openapi-validator๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ฐœ๊ฒฌํ•œ ์ธ์ฆ ๊ด€๋ จ ์ด์Šˆ๋ฅผ ๋‹ค๋ค˜์—ˆ๋Š”๋ฐ์š”, ๊ฐ„๋‹จํžˆ ์š”์•ฝ๋“œ๋ฆฌ๋ฉด:

Express.js๋กœ REST API๋ฅผ ๊ตฌ์ถ•ํ•˜๋ฉฐ JWT๋ฅผ http-only ์ฟ ํ‚ค๋กœ ์ „๋‹ฌํ•˜๋ ค ํ–ˆ๋Š”๋ฐ,
Authorization ํ—ค๋”์™€ CSRF ํ† ํฐ ๊ฒ€์ฆ์—์„œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

ํŠนํžˆ validateHttp() ๋ฉ”์„œ๋“œ๊ฐ€ Authorization ํ—ค๋”๋ฅผ ํ•„์ˆ˜๋กœ ์š”๊ตฌํ•˜๋Š” ๋ฐ”๋žŒ์—,
์ฟ ํ‚ค๋กœ JWT๋ฅผ ์ „๋‹ฌํ•˜๋Š” ์ƒํ™ฉ์—์„œ๋„ ํ—ค๋”๊ฐ€ ํ•„์š”ํ•œ ์ƒํ™ฉ์ด ๋์ฃ .


์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•ด๋ณด๋‹ˆ SecuritySchemes์™€ AuthValidator ํด๋ž˜์Šค์˜ validateHttp ๋ฉ”์„œ๋“œ์—์„œ ์ด ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ๊ณ ,
type: http๋กœ ์„ค์ •๋œ ์Šคํ‚ด์—์„œ Authorization ํ—ค๋”๋ฅผ ๊ฐ•์ œํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด Pull Request๋ฅผ ์ œ์ถœํ–ˆ๊ณ , ์—ฌ๋Ÿฌ ๋ฒˆ์˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ์™€ ์ˆ˜์ • ๊ณผ์ •์„ ๊ฑฐ์ณ ๋งˆ์นจ๋‚ด Merge๊ฐ€ ๋˜์–ด express-openapi-validator์˜ Contributor๊ฐ€ ๋œ ์—ฌ์ •์„ ์˜ค๋Š˜ ์ƒ์„ธํžˆ ๊ณต์œ ๋“œ๋ฆฌ๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค! ๐ŸŽ‰

๐ŸŽฏ ์ฒซ ๋ฒˆ์งธ ์‹œ๋„: ์ฟ ํ‚ค ์ง€์› ์ถ”๊ฐ€

์ฒ˜์Œ์—๋Š” ๋‹จ์ˆœํžˆ Authorization ํ—ค๋” ์™ธ์—๋„ ์ฟ ํ‚ค๋ฅผ ํ†ตํ•œ ์ธ์ฆ์„ ์ง€์›ํ•˜๋„๋ก ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค:

if (!authHeader && !authCookie) {
  throw Error(`Authorization header or cookie required`);
}

if (type === 'bearer') {
  if (authHeader && !authHeader.includes('bearer')) {
    throw Error(`Authorization header with scheme 'Bearer' required`);
  }

  if (!authHeader && authCookie === undefined) {
    throw Error(`Bearer token required in authorization header or cookie`);
  }
}

๐Ÿ’ก ์ฝ”๋“œ ๋ฆฌ๋ทฐ ํ”ผ๋“œ๋ฐฑ

์ œ๊ฐ€ ์ œ์ถœํ•œ PR์— ๋Œ€ํ•ด ๋ฉ”์ธํ…Œ์ด๋„ˆ๊ป˜์„œ ์ค‘์š”ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ฃผ์…จ์Šต๋‹ˆ๋‹ค:

  1. OpenAPI ๋ช…์„ธ์— ๋”ฐ๋ฅด๋ฉด, in: cookie๋กœ ์ง€์ •๋œ ๊ฒฝ์šฐ์™€ ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ๋ฅผ ๊ตฌ๋ถ„ํ•ด์•ผ ํ•จ
  2. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋„ ๊ฐ๊ฐ์˜ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๋‹ค๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ๋˜์–ด์•ผ ํ•จ
    • ์ผ๋ฐ˜์ ์ธ ๊ฒฝ์šฐ: "Authorization header required"
    • ์ฟ ํ‚ค ์ธ์ฆ์˜ ๊ฒฝ์šฐ: "Cookie authentication required"

๐Ÿ”จ ๋‘ ๋ฒˆ์งธ ์‹œ๋„: ๋ช…์„ธ ๊ธฐ๋ฐ˜ ์ˆ˜์ •

ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ–ˆ์ง€๋งŒ, ์—ฌ๊ธฐ์„œ ๋˜ ํ•˜๋‚˜์˜ ์‹ค์ˆ˜๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์กด ์ฝ”๋“œ ์Šคํƒ€์ผ์„ ํ•ด์น˜๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ํฌํ•จ๋œ ๊ฒƒ์ด์—ˆ์ฃ ..

 

์˜ˆ๋ฅผ ๋“ค์–ด ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋‚˜ ๋“ค์—ฌ์“ฐ๊ธฐ, ๊ด„ํ˜ธ ์œ„์น˜ ๋“ฑ์ด ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ์กด ์Šคํƒ€์ผ๊ณผ ๋‹ฌ๋ž์ฃ .

// ์ œ๊ฐ€ ์ˆ˜์ •ํ•œ ์ฝ”๋“œ
function extractErrorsFromResults(
  results: (SecurityHandlerResult | SecurityHandlerResult[])[],
) {
  return results
    .map((result) => {
      if (Array.isArray(result)) {
        return result.map((it) => it).filter((it) => !it.success);
      }
      return [result].filter((it) => !it.success);
    })
    .flatMap((it) => [...it]);
}

// ์›๋ž˜ ํ”„๋กœ์ ํŠธ ์Šคํƒ€์ผ
function extractErrorsFromResults(results: (SecurityHandlerResult | SecurityHandlerResult[])[]) {
  return results.map(result => {
    if (Array.isArray(result)) {
      return result.map(it => it).filter(it => !it.success);
    }
    return [result].filter(it => !it.success);
  }).flatMap(it => [...it]);
}

 

์ด๋ฅผ ๋น ๋ฅด๊ฒŒ ์›๋ž˜ ํ”„๋กœ์ ํŠธ์˜ ์Šคํƒ€์ผ๋Œ€๋กœ ๋˜๋Œ๋ฆฌ๊ณ  ์ปค๋ฐ‹์„ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์—ฌ๊ธฐ์„œ ๋์ด ์•„๋‹ˆ์—ˆ์ฃ . CI ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ ค๋ณด๋‹ˆ ๋‘ ๊ฐ€์ง€ ์‹ฌ๊ฐํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค:

๐Ÿ› ํ…Œ์ŠคํŠธ ์‹คํŒจ ๋ฐœ๊ฒฌ

์ดํ›„ CI์—์„œ ๋‘ ๊ฐ€์ง€ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจํ•œ๋‹ค๋Š” ๊ฒƒ์„ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค:

  1. Basic ์ธ์ฆ์—์„œ ํ—ค๋”๊ฐ€ ์—†์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ
  2. undefined.includes() ํ˜ธ์ถœ๋กœ ์ธํ•œ ์—๋Ÿฌ
AssertionError: expected 'Cannot read properties of undefined (…' 
to equal 'Authorization header required'

 

๊ฒฐ๋ก ์€ authHeader๊ฐ€ undefined์ผ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋ฌธ์ œ๋กœ ์ธํ•œ ํ…Œ์ŠคํŠธ ์‹คํŒจ์˜€์Šต๋‹ˆ๋‹ค.

โœจ ์ตœ์ข… ํ•ด๊ฒฐ์ฑ…

๋ชจ๋“  ํ”ผ๋“œ๋ฐฑ๊ณผ ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค๋ฅผ ๊ณ ๋ คํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ฝ”๋“œ๋ฅผ ์ตœ์ข… ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค:

private validateHttp(): void {
  if (['http'].includes(scheme.type.toLowerCase())) {
    const authHeader = req.headers['authorization']?.toLowerCase();
    const authCookie = req.cookies[scheme.name] || req.signedCookies?.[scheme.name];

    const type = scheme.scheme?.toLowerCase();
    if (type === 'bearer') {
      if (authHeader && !authHeader.includes('bearer')) {
        throw Error(`Authorization header with scheme 'Bearer' required`);
      }
      
      if (!authHeader && !authCookie) {
        throw Error(scheme.in === 'cookie' 
          ? `Cookie authentication required`
          : `Authorization header required`);
      }
    }

    if (type === 'basic') {
      if (!authHeader) {
        throw Error(`Authorization header required`);
      }
      if (!authHeader.includes('basic')) {
        throw Error(`Authorization header with scheme 'Basic' required`);
      }
    }
  }
}

์ฃผ์š” ๊ฐœ์„ ์‚ฌํ•ญ:

  1. Basic ์ธ์ฆ๊ณผ Bearer ์ธ์ฆ์˜ ๋ช…ํ™•ํ•œ ๊ตฌ๋ถ„
  2. ์ฟ ํ‚ค ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ง€์› (OpenAPI ๋ช…์„ธ ์ค€์ˆ˜)
  3. ์ ์ ˆํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ
  4. undefined ์ฒดํฌ ๊ฐ•ํ™”

๐ŸŽ“ ๋ฐฐ์šด ์ 

  1. OpenAPI ๋ช…์„ธ๋ฅผ ๋” ๊ผผ๊ผผํžˆ ์‚ดํŽด๋ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค
  2. ํ”„๋กœ์ ํŠธ์˜ ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰์˜ ์ค‘์š”์„ฑ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ๊นจ๋‹ฌ์•˜์Šต๋‹ˆ๋‹ค
  3. ์ฝ”๋“œ ์Šคํƒ€์ผ ๊ฐ€์ด๋“œ๋ฅผ ์ค€์ˆ˜ํ•˜๋Š” ๊ฒƒ์˜ ์ค‘์š”์„ฑ์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค

์ด๋ ‡๊ฒŒ ํ•ด์„œ express-openapi-validator์— ์ž‘์€ ๊ธฐ์—ฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์—ˆ๋„ค์š”. ์˜คํ”ˆ์†Œ์Šค ๊ธฐ์—ฌ๋Š” ์–ธ์ œ๋‚˜ ์ƒˆ๋กœ์šด ๋ฐฐ์›€์˜ ๊ธฐํšŒ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ์—๋Š” ๋˜ ์–ด๋–ค ์žฌ๋ฏธ์žˆ๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ฒŒ ๋ ์ง€ ๊ธฐ๋Œ€๊ฐ€ ๋ฉ๋‹ˆ๋‹ค! ๐Ÿ˜Š

React ๊ณต์‹๋ฌธ์„œ!!

๋“ค์–ด๊ฐ€๋ฉฐ

์ตœ๊ทผ Allini ํ”„๋กœ์ ํŠธ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉด์„œ ๋ช‡ ๊ฐ€์ง€ ๊ณ ๋ฏผ์ด ์ƒ๊ฒผ์Šต๋‹ˆ๋‹ค.

over-fetching, waterfall ํ˜„์ƒ์œผ๋กœ ์ธํ•œ ์„ฑ๋Šฅ ์ €ํ•˜, ๊ทธ๋ฆฌ๊ณ  ๋ฌด์—‡๋ณด๋‹ค ๋น ๋ฅธ ์‹œ์žฅ ๊ฒ€์ฆ์„ ์œ„ํ•ด ๋‹ค์–‘ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ฆ๊ฐ€ ๋ฌธ์ œ์˜€์ฃ .

 

์ข‹์€ ๊ฐœ๋ฐœ์ž๋Š” ๋‹จ์ˆœํžˆ ๋ชจ๋“  ๊ฒƒ์„ ์†์ˆ˜ ์ฒ˜์Œ๋ถ€ํ„ฐ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ ์ ˆํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๊ณ  ์กฐ์žกํ•ด ๋น ๋ฅด๊ฒŒ ์ œํ’ˆ์„ ๋งŒ๋“ค์–ด๋‚ด๋Š” ๋Šฅ๋ ฅ์ด ์ค‘์š”ํ•˜๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ์ ‘๊ทผ์€ ํ•„์—ฐ์ ์œผ๋กœ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ฆ๊ฐ€๋ผ๋Š” ๋ฌธ์ œ๋ฅผ ๋™๋ฐ˜ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ , ์ด๋Š” ์ œ๊ฐ€ React ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์— ๊ด€์‹ฌ์„ ๊ฐ–๊ฒŒ ๋œ ์ฃผ๋œ ๊ณ„๊ธฐ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

 

1. React ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ํ˜„์žฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€

ํ˜„์žฌ ์ƒํƒœ

  • React 18 ๋ฒ„์ „: ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๋ฏธ์ง€์›
  • React 19 RC(Release Candidate): ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ง€์›
 

ํ•˜์ง€๋งŒ ํ˜„์žฌ React 18.2.0์„ ์‚ฌ์šฉ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—์„œ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ธฐ๋Š” ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

React 19๋กœ์˜ ์—…๊ทธ๋ ˆ์ด๋“œ๋ฅผ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ์ง€๋งŒ, RC ๋ฒ„์ „์ด๊ธฐ ๋•Œ๋ฌธ์— ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ์˜ ์‚ฌ์šฉ์€ ์‹ ์ค‘ํžˆ ๊ฒฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

2. SSR๊ณผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๊ด€๊ณ„

ํ•™์Šต ์ดˆ๊ธฐ์— ๊ฐ€์žฅ ํ—ท๊ฐˆ๋ ธ๋˜ ๊ฐœ๋…์ž…๋‹ˆ๋‹ค!

SSR (Server-Side Rendering)

  • ๋ชฉ์ : ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋“œ ์„ฑ๋Šฅ ๊ฐœ์„  ๋ฐ SEO ์ตœ์ ํ™”
  • ๋™์ž‘ ๋ฐฉ์‹:
    • ์„œ๋ฒ„์—์„œ ์ „์ฒด ํŽ˜์ด์ง€์˜ ์ดˆ๊ธฐ HTML์„ ์ƒ์„ฑ
    • ํด๋ผ์ด์–ธํŠธ๋กœ HTML๊ณผ JS ๋ฒˆ๋“ค์„ ์ „์†ก
    • ํด๋ผ์ด์–ธํŠธ์—์„œ hydration ๊ณผ์ •์„ ํ†ตํ•ด ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ์•ฑ์œผ๋กœ ์ „ํ™˜

์ด์ „ React ๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด, React์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง ๋ฉ”์„œ๋“œ๋“ค์€ ํฌ๊ฒŒ ์„ธ ๊ฐ€์ง€ ํ™˜๊ฒฝ์œผ๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค:

 

Node.js Streams ํ™˜๊ฒฝ์šฉ:

  • renderToPipeableStream()
  • renderToNodeStream() (Deprecated)
  • renderToStaticNodeStream()

Web Streams ํ™˜๊ฒฝ์šฉ (๋ธŒ๋ผ์šฐ์ €, Deno, modern edge runtimes):

  • renderToReadableStream()

์ŠคํŠธ๋ฆผ์„ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ™˜๊ฒฝ์šฉ:

  • renderToString()
  • renderToStaticMarkup()

๊ธฐ์กด SSR์˜ ๋™์ž‘ ๋ฐฉ์‹์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// ๊ธฐ์กด SSR์˜ ๋™์ž‘ ๋ฐฉ์‹
// 1. ์„œ๋ฒ„์—์„œ ์ดˆ๊ธฐ ๋ Œ๋”๋ง
const app = ReactDOMServer.renderToPipeableStream(<App />);
// ํ•œ๋ฒˆ์— ์ „์ฒด HTML์„ ์ƒ์„ฑํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก

// 2. HTML ๋ฌธ์„œ์— ์‚ฝ์ž…
const html = `
  <!doctype html>
  <html>
    <body>
      <div id="root">${app}</div>
      <script src="/bundle.js"></script>
    </body>
  </html>
`;

 

 

๊ณต์‹ ๋ฌธ์„œ์— ๋”ฐ๋ฅด๋ฉด, renderToPipeableStream์„ ํ†ตํ•ด ์ดˆ๊ธฐ ์…ธ(shell)์„ ๋ Œ๋”๋งํ•˜๊ณ  ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

renderToPipeableStream์˜ ๊ธฐ๋ณธ ๊ฐœ๋…์€ ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • Node.js ํ™˜๊ฒฝ์—์„œ React ํŠธ๋ฆฌ๋ฅผ HTML ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” API์ž…๋‹ˆ๋‹ค.
  • ์ ์ง„์ ์ธ ๋กœ๋”ฉ์ด ๊ฐ€๋Šฅํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ SSR์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์‹œ ์ฝ”๋“œ

// ๋ฐœ์ „๋œ SSR (์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฉ์‹)
const { pipe } = renderToPipeableStream(<App />, {
  bootstrapScripts: ['/main.js'],
  onShellReady() {
    // 1. ๋จผ์ € ๊ธฐ๋ณธ ๋ผˆ๋Œ€(shell)๋ฅผ ๋ณด๋ƒ„
    // ์ดˆ๊ธฐ shell(Suspense ๊ฒฝ๊ณ„ ์œ„์˜ ์ฝ˜ํ…์ธ )์ด ์ค€๋น„๋˜๋ฉด ์ŠคํŠธ๋ฆฌ๋ฐ ์‹œ์ž‘

    pipe(response);
  },
  onAllReady() {
    // ๋ชจ๋“  ์ฝ˜ํ…์ธ ๊ฐ€ ์ค€๋น„๋˜๋ฉด ํ˜ธ์ถœ (ํฌ๋กค๋Ÿฌ๋‚˜ ์ •์  ์ƒ์„ฑ์šฉ)
  }
});

 

๊ธฐ์กด์˜ renderToString์€ ์ œํ•œ์ ์ธ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๋ฐ˜๋ฉด, renderToPipeableStream์€ Suspense๋ฅผ ์™„์ „ํžˆ ์ง€์›ํ•˜๊ณ  HTML ์ŠคํŠธ๋ฆฌ๋ฐ์ด ๊ฐ€๋Šฅํ•œ ๋” ๋ฐœ์ „๋œ ํ˜•ํƒœ์˜ SSR ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

์ฃผ์š” ์ฐจ์ด์ :

  • ๊ธฐ์กด SSR: ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค ๋ชจ์•„์„œ ํ•œ๋ฒˆ์— ๋ณด๋ƒ„ (๋А๋ฆผ)
  • ์ƒˆ๋กœ์šด SSR:
    1. ๋จผ์ € ํŽ˜์ด์ง€์˜ ๊ธฐ๋ณธ ๋ผˆ๋Œ€๋ฅผ ๋ณด๋‚ด๊ณ 
    2. ๋‚˜๋จธ์ง€ ๋‚ด์šฉ์€ ์ค€๋น„๋˜๋Š” ๋Œ€๋กœ ์กฐ๊ธˆ์”ฉ ๋ณด๋ƒ„ (๋” ๋น ๋ฆ„)

 

์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, ๊ธฐ์กด SSR์€ "์‹๋‹น์—์„œ ๋ชจ๋“  ์š”๋ฆฌ๊ฐ€ ์™„์„ฑ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ํ•œ๋ฒˆ์— ์„œ๋น™"ํ•˜๋Š” ๊ฒƒ์ด๊ณ , ์ƒˆ๋กœ์šด ๋ฐฉ์‹์€ "์ค€๋น„๋œ ๋ฉ”๋‰ด๋ถ€ํ„ฐ ๋จผ์ € ์„œ๋น™ํ•˜๊ณ , ๋‚˜๋จธ์ง€๋Š” ์™„์„ฑ๋˜๋Š” ๋Œ€๋กœ ๊ฐ€์ ธ๋‹ค์ฃผ๋Š”" ๊ฒƒ๊ณผ ๋น„์Šทํ•ฉ๋‹ˆ๋‹ค.

 

React ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ (RSC)

  • ๋ชฉ์ : ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™” ๋ฐ ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ์ง์ ‘ ์ ‘๊ทผ
  • ๋™์ž‘ ๋ฐฉ์‹:
    • ํŠน์ • ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๋„๋ก ์ง€์ •
    • ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋œ ๊ฒฐ๊ณผ๋งŒ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก
    • JS ๋ฒˆ๋“ค์— ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ฝ”๋“œ๋Š” ๋ฏธํฌํ•จ

๋‘ ๊ธฐ์ˆ ์˜ ์ƒํ˜ธ๋ณด์™„์  ๊ด€๊ณ„

SSR๊ณผ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋กœ๋ฅผ ๋Œ€์ฒดํ•˜๋Š” ๊ธฐ์ˆ ์ด ์•„๋‹Œ, ๋ณด์™„ํ•˜๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค:

  1. ์—ญํ•  ๋ถ„๋‹ด
    • SSR: ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ „์ฒด HTML ์ƒ์„ฑ
    • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ: ํŠน์ • ์ปดํฌ๋„ŒํŠธ๋“ค์˜ ์„œ๋ฒ„ ์‹คํ–‰ ๋ฐ ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™”

 

3. ํ”„๋ ˆ์ž„์›Œํฌ๋“ค์˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„

Next.js์˜ ์ ‘๊ทผ

Next.js๋Š” ์ž์ฒด์ ์ธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ตฌํ˜„์„ ํ†ตํ•ด React์˜ ์‹คํ—˜์  ๊ธฐ๋Šฅ์„ ์•ˆ์ •์ ์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

  1. React ์ฝ”์–ด ํŒ€๊ณผ Next.js ํŒ€์˜ ํ˜‘๋ ฅ
    • React ์ฝ”์–ด ํŒ€์˜ ์ฃผ์š” ๋ฉค๋ฒ„๋“ค(Joe Savona, Sebastian Markbåge ๋“ฑ)์ด Vercel(Next.js)์— ํ•ฉ๋ฅ˜
    • ์ด๋ฅผ ํ†ตํ•ด React์˜ ์‹คํ—˜์  ๊ธฐ๋Šฅ์„ Next.js์—์„œ ๋จผ์ € ์•ˆ์ •์ ์œผ๋กœ ๊ตฌํ˜„ ๊ฐ€๋Šฅ
  2. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฐœ์ „ ๊ณผ์ •
    • 2020๋…„: React ํŒ€์ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ฐœ๋… ์ฒซ ๋ฐœํ‘œ
    • React 18: ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ๊ธฐ์ดˆ ์ธํ”„๋ผ ํฌํ•จ, but ์™„์ „ํ•œ ๊ตฌํ˜„์€ ์•„๋‹˜
    • Next.js 13: App Router์™€ ํ•จ๊ป˜ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ „๋ฉด ๋„์ž…

๊ธฐ์ˆ ์  ๊ตฌํ˜„ ๋ฐฉ์‹

React์˜ Server Components์— ๋Œ€ํ•œ ๊ณต์‹๋ฌธ์„œ๋ฅผ ๋ณด๋ฉด,

๋ฌธ์„œ์—์„œ๋Š” Server Components ๊ตฌํ˜„์— ๋Œ€ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์–ธ๊ธ‰ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

"To support React Server Components as a bundler or framework, we recommend pinning to a specific React version, or using the Canary release. We will continue working with bundlers and frameworks to stabilize the APIs used to implement React Server Components in the future."

์ด๋Š” Server Components ๊ตฌํ˜„์„ ์œ„ํ•œ ๋ฒˆ๋“ค๋Ÿฌ API๊ฐ€ ์•„์ง ์™„์ „ํžˆ ์•ˆ์ •ํ™”๋˜์ง€ ์•Š์•˜์Œ์„ ์‹œ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

 

Next.js๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค:

// next.config.js
{
  webpack: (config, { isServer }) => {
    if (isServer) {
      // ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ
      config.plugins.push(new webpack.DefinePlugin({
        // marked, sanitize-html ๊ฐ™์€ ์„œ๋ฒ„ ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด
        // ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌ
        'process.env.SERVER_ONLY': JSON.stringify(true)
      }));
      
      config.module.rules.push({
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
            plugins: [
              // async ์ปดํฌ๋„ŒํŠธ ์ง€์›์„ ์œ„ํ•œ ๋ณ€ํ™˜
              '@babel/plugin-syntax-top-level-await',
              // Server Component์˜ import ๊ตฌ๋ฌธ ์ฒ˜๋ฆฌ
              ['@babel/plugin-transform-modules-commonjs', {
                importInterop: 'node'
              }]
            ]
          }
        }
      });
    } else {
      // ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ์ฒ˜๋ฆฌ
      config.module.rules.push({
        test: /\.(js|jsx|ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react'],
            plugins: [
              // "use client" ์ง€์‹œ์–ด๊ฐ€ ์žˆ๋Š” ํŒŒ์ผ๋งŒ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ
              ['./plugins/client-directive', {}],
              // Server Component์˜ ์ถœ๋ ฅ์„ ํด๋ผ์ด์–ธํŠธ์—์„œ hydrateํ•˜๊ธฐ ์œ„ํ•œ ๋ณ€ํ™˜
              ['./plugins/server-reference', {}]
            ]
          }
        }
      });
    }
    return config;
  }
}

// Server Component ๋ Œ๋”๋ง ํ•จ์ˆ˜
async function renderServerComponent(Component, props) {
  // ๋ฌธ์„œ์— ์–ธ๊ธ‰๋œ ๊ฒƒ์ฒ˜๋Ÿผ build time ๋˜๋Š” request time์— ์‹คํ–‰ ๊ฐ€๋Šฅ
  const stream = await ReactServerDOM.renderToReadableStream(
    <Component {...props} />,
    // ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ๋ฒˆ๋“ค ์ •๋ณด๋ฅผ ๋‹ด์€ ๋งคํ•‘
    webpackMap
  );
  
  // React์˜ Flight ํฌ๋งท์œผ๋กœ ์ง๋ ฌํ™”
  return encodeRSCPayload(stream);
}

 

์ด ๊ตฌํ˜„์€ ๊ณต์‹ ๋ฌธ์„œ์˜ ๋‹ค์Œ ๊ฐœ๋…๋“ค์„ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค:

  1. "Server Components are a new type of Component that renders ahead of time, before bundling, in an environment separate from your client app or SSR server."
    • webpack ์„ค์ •์—์„œ isServer ํ”Œ๋ž˜๊ทธ๋กœ ํ™˜๊ฒฝ์„ ๋ถ„๋ฆฌ
  2. "Server Components can run once at build time on your CI server, or they can be run for each request using a web server."
    • renderServerComponent ํ•จ์ˆ˜๊ฐ€ ์ด ๋‘ ๊ฐ€์ง€ ์ผ€์ด์Šค๋ฅผ ๋ชจ๋‘ ์ง€์›
  3. "Async Components are a new feature of Server Components that allow you to await in render."
    • ์„œ๋ฒ„ ์ธก babel ์„ค์ •์— @babel/plugin-syntax-top-level-await ํฌํ•จ

ํ•ต์‹ฌ ๊ตฌํ˜„ ์š”์†Œ

  1. RSC ํฌ๋งท
    • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ ํŠน๋ณ„ํ•œ ํฌ๋งท์œผ๋กœ ์ง๋ ฌํ™”
    • JSON์ด ์•„๋‹Œ ํŠน๋ณ„ํ•œ ๋ฐ”์ด๋„ˆ๋ฆฌ ํ˜•์‹ ์‚ฌ์šฉ
    • React Flight ์•„ํ‚คํ…์ฒ˜ ํ™œ์šฉ
  2. ๋ฒˆ๋“ค๋ง ์ตœ์ ํ™”
    • ์„œ๋ฒ„ ์ „์šฉ ์ฝ”๋“œ๋ฅผ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์—์„œ ์ œ์™ธ
    • ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋งŒ ์„ ํƒ์ ์œผ๋กœ ๋ฒˆ๋“ค๋ง
  3. ์ŠคํŠธ๋ฆฌ๋ฐ ์ง€์›
    • React Suspense์™€ ํ†ตํ•ฉ
    • ์ ์ง„์ ์ธ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ๊ตฌํ˜„

์ดํ•ด๋ฅผ ์œ„ํ•ด ๊ธฐ๋ณธ์ ์ธ ํ‹€์— ๋งž์ถฐ ๊ฐ„์†Œํ™”ํ•ด ๊ตฌํ˜„ํ•ด ๋ดค์Šต๋‹ˆ๋‹ค!
ํ˜น์‹œ ํ‹€๋ฆฐ ๋ถ€๋ถ„์ด ์žˆ๋‹ค๋ฉด ํŽธํ•˜๊ฒŒ ๋Œ“๊ธ€ ์ฃผ์‹œ๋ฉด ๊ฐ์‚ฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!

Remix์˜ ์ ‘๊ทผ

Remix๋Š” ๋ผ์šฐํ„ฐ ๊ธฐ๋ฐ˜์˜ ์„œ๋ฒ„ ์ค‘์‹ฌ ์•„ํ‚คํ…์ฒ˜๋ฅผ ์ฑ„ํƒํ•˜์—ฌ ์œ ์‚ฌํ•œ ์ด์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

// Remix์˜ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋กœ์ง ์˜ˆ์‹œ
export async function loader({ request }) {
  const data = await getProducts();
  return json(data);
}

export default function Products() {
  const products = useLoaderData();
  return (
    <div>
      <h1>Products</h1>
      <ProductList products={products} />
    </div>
  );
}

 

4. Next.js ์•ฑ ๋ผ์šฐํ„ฐ์™€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๊ด€๊ณ„

Next.js์˜ ์•ฑ ๋ผ์šฐํ„ฐ๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ฑ„ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.  ๋งค์šฐ ์ค‘์š”ํ•œ ์•„ํ‚คํ…์ฒ˜์  ๊ฒฐ์ •์ด์ฃ ?!

๊ธฐ๋ณธ ๊ตฌ์กฐ

// app/layout.jsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>{children}</body>
    </html>
  );
}

// app/page.jsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)
async function Home() {
  const data = await getData(); // ์„œ๋ฒ„์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ fetch
  
  return (
    <main>
      <h1>Welcome</h1>
      <ClientComponent data={data} />
    </main>
  );
}

// components/client-component.jsx
'use client';

function ClientComponent({ data }) {
  // ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ ์ธํ„ฐ๋ž™์…˜์ด ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ
  return <div>{/* ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ UI */}</div>;
}

 

์ฃผ์š” ํŠน์ง•

์ž๋™ ์ฝ”๋“œ ๋ถ„ํ• 

// app/products/page.jsx
import { ProductList } from './components/product-list';

async function ProductsPage() {
  const products = await fetchProducts(); // ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰
  return <ProductList products={products} />;
}

// ์ด ์ปดํฌ๋„ŒํŠธ์™€ ๊ด€๋ จ๋œ ์„œ๋ฒ„ ๋กœ์ง์€ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š์Œ

 

 

์ŠคํŠธ๋ฆฌ๋ฐ๊ณผ Suspense ํ†ตํ•ฉ

// app/dashboard/page.jsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<LoadingUI />}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}
 

๋งˆ์น˜๋ฉฐ

ํ˜„์žฌ ์ œ๊ฐ€ ์ง„ํ–‰ ์ค‘์ธ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” React 18.2.0์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์–ด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ธฐ๋Š” ์–ด๋ ค์šด ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ๊ณ ๊ฐ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›์€ ํ›„ Next.js๋กœ์˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๊ณ ๋ คํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ์ด์ ์„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ข‹์€ ๊ธฐํšŒ๊ฐ€ ๋  ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

ํ”„๋กœ์ ํŠธ๊ฐ€ ์ปค์ง€๋ฉฐ, ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™”์™€ ์„œ๋ฒ„ ๋ฆฌ์†Œ์Šค ํ™œ์šฉ ์ธก๋ฉด์—์„œ ํฐ ์ด์ ์„ ์ œ๊ณตํ•  ๊ฒƒ์œผ๋กœ ๊ธฐ๋Œ€๋ฉ๋‹ˆ๋‹ค!

 

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
React 19 RC!!!
React RFC ๊นƒํ—™
Next.js App Router RFC ๊นƒํ—™
Next.js Server Components ๊ณต์‹๋ฌธ์„œ
React CoreํŒ€ Dan Abramov์˜ Server Components ์†Œ๊ฐœ ๋ฐœํ‘œ
์นด์นด์˜คํŽ˜์ด - React Server Components
์š”์ฆ˜ IT - React Server Components
freeCodeCamp - React Server Components

 

๋ฌดํ•œ ์Šคํฌ๋กค๊ณผ ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์„ ๋„์ž… ์ค‘์ธ ํŽ˜์ด์ง€์ž…๋‹ˆ๋‹ค!

 

ํ”„๋กœ์ ํŠธ์— ๋„์ž…๋  ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ฐœ๋ฐœ ์ค‘์ธ ์ƒํ™ฉ์ž…๋‹ˆ๋‹ค.
๊ฐœ๋ฐœ์„ ์ง„ํ–‰ํ•˜๋˜ ์ค‘ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฌด์ˆ˜ํžˆ ๋งŽ์•„์งˆ ๊ฒฝ์šฐ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์ง„ ์•Š์„๊นŒ๋ผ๋Š” ์ƒ๊ฐ์— ๋ฌดํ•œ ์Šคํฌ๋กค์— ๋Œ€ํ•œ ์ตœ์ ํ™”๋ฅผ ๊ณ ๋ฏผํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!
์—ฌ๋Ÿฌ ์ตœ์ ํ™” ์ค‘ ํ•˜๋‚˜๋Š” ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์— ๋Œ€ํ•ด ํŒŒํŠธ๋ฅผ ๋‚˜๋ˆ  ๊ธฐ๋กํ•ด๋‘๋ ค ํ•ด์š”!

 

๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค ์ด๋ž€?

๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค(Virtual Scroll)์€ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ์ˆ ๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ๋กœ ๋ณด๊ณ  ์žˆ๋Š” ํ•ญ๋ชฉ๋งŒ ๋ Œ๋”๋งํ•˜์—ฌ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค.

 

ํŠนํžˆ, ๋ฆฌ์ŠคํŠธ๋‚˜ ๊ทธ๋ฆฌ๋“œ ํ˜•์‹์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

์ด๋ฒˆ ๋ธ”๋กœ๊ทธ์—์„œ๋Š” ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์„ ์‚ฌ์šฉํ•ด์•ผ ํ•  ์ƒํ™ฉ๊ณผ, ์‹ค์ œ ์ƒ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ์˜ ๊ตฌํ˜„ ๋ฐฉ์‹์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ƒ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ๋Œ€ํ•œ ์„ค๋ช…์€ ๋ณ„๋„์˜ ๊ธ€๋กœ ๋ถ„๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค!

 

๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์„ ์–ด๋А ์ƒํ™ฉ์—์„œ ์จ์•ผ ํ• ๊นŒ?

๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์€ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ํ•„์ˆ˜์ ์ด๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์‚ฌ์šฉ์ž์—๊ฒŒ ์ˆ˜์ฒœ ์ˆ˜๋งŒ ๊ฐœ์˜ ํ•ญ๋ชฉ์ด ํฌํ•จ๋œ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ, ๋ชจ๋“  ํ•ญ๋ชฉ์„ ํ•œ ๋ฒˆ์— ๋ Œ๋”๋งํ•˜๋ฉด ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋Ÿด ๋•Œ, ํ˜„์žฌ ํ™”๋ฉด์— ๋ณด์ด๋Š” ํ•ญ๋ชฉ๋งŒ ๋ Œ๋”๋งํ•จ์œผ๋กœ์จ ๋ Œ๋”๋ง ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํฌ๋กค ์„ฑ๋Šฅ ๊ฐœ์„ 

์ผ๋ฐ˜์ ์ธ ์Šคํฌ๋กค ๋ฐฉ์‹์—์„œ๋Š” ๋ชจ๋“  ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์ด DOM์— ์กด์žฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ ํ™”๋ฉด์— ๋ณด์ด๋Š” ํ•ญ๋ชฉ์€ ๋ฐ”๋€Œ์ง€๋งŒ, DOM์—์„œ ๋ชจ๋“  ์š”์†Œ๊ฐ€ ๊ณ„์† ์œ ์ง€๋˜๋ฏ€๋กœ ์ง€์›Œ์ง€๊ฑฐ๋‚˜ ์ถ”๊ฐ€๋˜๋Š” ๊ณผ์ •์ด ์—†์Šต๋‹ˆ๋‹ค.

๋Œ€์‹ , ๋ธŒ๋ผ์šฐ์ €๋Š” ์Šคํฌ๋กค์— ๋”ฐ๋ผ ๋ณด์ด์ง€ ์•Š๋Š” ํ•ญ๋ชฉ๋“ค์„ ์—ฌ์ „ํžˆ ๋ฉ”๋ชจ๋ฆฌ์— ๋‘๊ณ  ์žˆ์–ด, ์ „์ฒด ๋ฆฌ์ŠคํŠธ์˜ ํฌ๊ธฐ๊ฐ€ ์ปค์งˆ์ˆ˜๋ก ๋ Œ๋”๋ง ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ฐ˜๋ฉด, ๊ฐ€์ƒ ์Šคํฌ๋กค์—์„œ๋Š” ์Šคํฌ๋กคํ•  ๋•Œ ํ˜„์žฌ ํ™”๋ฉด์— ๋ณด์ด๋Š” ํ•ญ๋ชฉ๋งŒ DOM์— ๋ Œ๋”๋งํ•˜๊ณ , ๋‚˜๋จธ์ง€ ํ•ญ๋ชฉ์€ DOM์—์„œ ์ง€์šฐ๊ฑฐ๋‚˜ ์ˆจ๊น๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•จ์œผ๋กœ์จ, ๋ฉ”๋ชจ๋ฆฌ์— ์œ ์ง€๋˜๋Š” ์š”์†Œ์˜ ์ˆ˜๊ฐ€ ์ค„์–ด๋“ค์–ด ์„ฑ๋Šฅ์ด ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ ํ•„์š”ํ•œ ๋งŒํผ์˜ ํ•ญ๋ชฉ๋งŒ ์ƒ์„ฑํ•˜๊ณ , ๋ณด์ด์ง€ ์•Š๋Š” ํ•ญ๋ชฉ์€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œํ•˜๊ฒŒ ๋˜์–ด, ๋ Œ๋”๋ง ๊ณผ์ •์—์„œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•  ์š”์†Œ์˜ ์ˆ˜๊ฐ€ ์ ์–ด์ง€๋ฏ€๋กœ ์Šคํฌ๋กค ์„ฑ๋Šฅ์ด ๋”์šฑ ์›ํ™œํ•ด์ง€๊ฒ ์ฃ ?!

 

๊ฒฐ๋ก ์ ์œผ๋กœ, ์ผ๋ฐ˜ ์Šคํฌ๋กค์—์„œ๋Š” DOM์˜ ํ•ญ๋ชฉ์ด ์ง€์›Œ์ง€์ง€ ์•Š์ง€๋งŒ, ๊ฐ€์ƒ ์Šคํฌ๋กค์€ ํ•„์š” ์—†๋Š” ํ•ญ๋ชฉ์„ ์ œ๊ฑฐํ•˜์—ฌ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ–ฅ์ƒ

ํŽ˜์ด์ง€ ๋‚ด์—์„œ์˜ ์›ํ™œํ•œ ์Šคํฌ๋กค๋ง ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด์„œ๋„ ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

๋ชจ๋“  ํ•ญ๋ชฉ์„ ํ•œ ๋ฒˆ์— ๋กœ๋“œํ•˜์ง€ ์•Š๊ณ , ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ๋กœ๋“œํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋น ๋ฅด๊ณ  ์›ํ™œํ•œ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋ฆฌ๋ Œ๋”๋ง ์„ฑ๋Šฅ ๋˜ํ•œ ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ถ€๋ถ„์ž…๋‹ˆ๋‹ค.

์ผ๋ฐ˜์ ์ธ ์Šคํฌ๋กค ๋ฐฉ์‹์—์„œ๋Š” ๋ชจ๋“  ๋ฆฌ์ŠคํŠธ ํ•ญ๋ชฉ์ด DOM์— ์กด์žฌํ•˜๋ฏ€๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ๋งˆ๋‹ค ๊ธฐ์กด ์š”์†Œ๊ฐ€ ์žฌ๋ Œ๋”๋ง๋˜๊ฑฐ๋‚˜ ์œ„์น˜๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ณผ์ •์—์„œ ๋ถˆํ•„์š”ํ•œ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜๋ฉด ์„ฑ๋Šฅ ์ €ํ•˜๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด, ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์—์„œ๋Š” ํ˜„์žฌ ํ™”๋ฉด์— ๋ณด์ด๋Š” ํ•ญ๋ชฉ๋งŒ ๋ Œ๋”๋งํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ฆฌ๋ Œ๋”๋ง์ด ํ•„์š”ํ•œ ํ•ญ๋ชฉ์˜ ์ˆ˜๊ฐ€ ์ค„์–ด๋“ค์–ด ์„ฑ๋Šฅ์ด ๊ฐœ์„ ๋ฉ๋‹ˆ๋‹ค.

 

๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋„ ํ™”๋ฉด์— ๋ณด์ด๋Š” ํ•ญ๋ชฉ๋งŒ์„ ํšจ์œจ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์–ด, ์ „์ฒด์ ์ธ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ํฌ๊ฒŒ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.

 

๊ฒฐ๊ณผ์ ์œผ๋กœ, ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ๋งค๋„๋Ÿฝ๊ณ  ๋น ๋ฅธ ์Šคํฌ๋กค๋ง ๊ฒฝํ—˜์„ ์ œ๊ณตํ•จ์œผ๋กœ์จ, ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฒฝํ—˜์€ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด๋‹ค ์ง๊ด€์ ์œผ๋กœ ํƒ์ƒ‰ํ•˜๊ณ , ํ•„์š”ํ•  ๋•Œ ๋น ๋ฅด๊ฒŒ ์ •๋ณด๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

 

์‹ค์ œ๋กœ ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์„ ์ƒ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋กœ ๋งŒ๋“  ๊ณณ์—์„  ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ–ˆ์„๊นŒ?

์ƒ์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ตฌํ˜„ ๋ฐฉ์‹

 react-window, react-virtualized, react-virtuoso ๊ด€๋ จ๋œ ๋ณ„๋„์˜ ๊ธ€๋กœ ๋ถ„๋ฆฌํ•˜์˜€์Šต๋‹ˆ๋‹ค!

 

๋งˆ๋ฌด๋ฆฌ

๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์€ ๋Œ€๋Ÿ‰์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ์‚ฌ์šฉ์ž์—๊ฒŒ ์›ํ™œํ•œ ์Šคํฌ๋กค๋ง ๊ฒฝํ—˜์„ ์ œ๊ณตํ• ๋•Œ ๋งค๋ ฅ์žˆ๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค!

๋‹ค์–‘ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ๊ฐ๊ธฐ ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์ ํ•ฉํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

 

์ €๋Š” ํ•™์Šต ๋ฐ ๊ฐœ์„ ์ ์„ ์ฐพ๊ธฐ์œ„ํ•ด ์Šค์Šค๋กœ ๊ตฌํ˜„์ค‘์— ์žˆ์Šต๋‹ˆ๋‹ค!

 

๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์ด ํ•„์š”ํ•œ ์ด์œ ์— ๋Œ€ํ•œ ๊ฒฝํ—˜

์‹ค์ œ๋กœ ํ˜„์žฌ ํšŒ์‚ฌ์—์„œ ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃจ๊ฒŒ ๋  ๊ฒฝ์šฐ, DOM์˜ ํฌ๊ธฐ๊ฐ€ ์ปค์ง์— ๋”ฐ๋ผ ์„ฑ๋Šฅ ์ €ํ•˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์—ˆ์–ด์š”.

 

์˜ˆ๋ฅผ ๋“ค์–ด, ๊ณ ๊ฐ๋‹น ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ 10๋งŒ ๊ฐœ์— ๋‹ฌํ•  ๊ฒฝ์šฐ, ์œ ์ €์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ธฐํ•˜๊ธ‰์ˆ˜์ ์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋ฉด์„œ ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ํ•„์š”ํ•ด์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ์ด์œ ๋กœ ๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์„ ๋„์ž…ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹น์žฅ์€ ๊ตฌํ˜„๋œ ํ”„๋กœ์ ํŠธ์— ๋ฌธ์ œ๊ฐ€ ์—†์–ด๋ณด์—ฌ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ , ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•œ ์„ ์ œ์ ์ธ ์กฐ์น˜๋กœ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

 

๋ฒ„์ธ„์–ผ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์—ฌ๋Ÿฌ ๊ธฐ๋ฒ•๋“ค์„ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์Šคํฌ๋กค์„ ํ†ตํ•ด ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•  ๋•Œ, ๋‚จ์€ ๊ฐ€์ƒ ์Šคํฌ๋กค ์•„์ดํ…œ์ด ์žˆ๋Š”์ง€ ํŒ๋‹จํ•˜๋Š” ๋กœ์ง์„ ํ†ตํ•ด ํ•„์š”ํ•œ ๋งŒํผ๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ•˜๋„๋ก ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๊ณผ์ •์—์„œ ๋ฐ์ดํ„ฐ์˜ ์–‘๊ณผ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ณต์žก์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ ์ตœ์ ์˜ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์†์ ์ธ ๋…ธ๋ ฅ์ค‘์ž…๋‹ˆ๋‹ค!

 

 

 

๐Ÿ“ƒ ์ฐธ๊ณ  ๋ฌธํ—Œ  
์˜ค๋Š˜์˜์ง‘ ๋‚ด ๋ฌดํ•œ ์Šคํฌ๋กค ๊ฐœ๋ฐœ๊ธฐ
React Window ๊นƒํ—™
React Virtualized ๊นƒํ—™
React Virtuoso ๊นƒํ—™

 

+ Recent posts