
React๋ Next.js๋ก ํ๋ก์ ํธ๋ฅผ ์งํํ๋ค ๋ณด๋ฉด ์ฒ์์ ๋จ์ํ๋ ์ปดํฌ๋ํธ๊ฐ ์ด๋์ 1,000์ค์ด ๋๊ณ ์๋๋ฉด ๋ค๋ฅธ ๋ถ๋ถ์์ ์ค๋ฅ๊ฐ ๋ฐ์ํ๋ ์คํ๊ฒํฐ ์ฝ๋๋ก ๋ณํด๋ฒ๋ฆฌ๊ณค ํด์.
๋ค๋ค ์ด๋ฐ ๊ฒฝํ ์์ผ์ ๊ฐ์?
์ด๋ฐ ๋ฌธ์ ๋ ์ฐ๋ฆฌ๋ง์ ๊ณ ๋ฏผ์ด ์๋๋ผ ์๋ง์ ์ ๋ฐฐ ๊ฐ๋ฐ์๋ค์ด ๊ฒช์ด์จ ๊ณตํต์ ์ธ ๋ฌธ์ ์์. ๊ทธ๋ฆฌ๊ณ ์ค๋ ๊ณ ๋ฏผ ๋์ ๋ง๋ค์ด์ง ํด๊ฒฐ์ฑ ์ค ํ๋๊ฐ ๋ฐ๋ก ๋์์ธ ํจํด(Design Pattern) ์ด์์.
ํจํด๋ง๋ค ์ด๋ค ๋ฌธ์ ๋ฅผ ํ๊ธฐ ์ํด ๋์๋์ง๋ฅผ ์ค์ฌ์ผ๋ก ๋ด์ฃผ์ธ์. ๋ฌด์กฐ๊ฑด ์ฐ๋ ๊ฒ ์๋๋ผ "์ด ์ํฉ์ด๋ฉด ์ด ํจํด"์ด๋ผ๋ ํ๋จ ๊ธฐ์ค์ ๊ฐ์ ธ๊ฐ๋ ๊ฒ ๋ชฉํ์์.
์ปดํฌ๋ํธ๊ฐ ์ปค์ง์๋ก "์ด ์ปดํฌ๋ํธ๊ฐ ๋ญ ๋ณด์ฌ์ฃผ๋ ๊ฑด์ง" "๋ญ ์ฒ๋ฆฌํ๋ ๊ฑด์ง"๊ฐ ๋ค์์ด๊ธฐ ์์ํด์. UI๋ฅผ ๋ฐ๊พธ๋ ค๋๋ฐ ๋ก์ง๋ ๊ฑด๋๋ ค์ผ ํ๊ณ ๋ฐ๋๋ก ๋ก์ง๋ง ์์ ํ๋ฉด ๋๋ ์ํฉ์์ UI ์ฝ๋๋ฅผ ๊ฐ์ด ์ฝ์ด์ผ ํ๋ ์์ด์์. Container-Presentation ํจํด์ ์ด ๋ ์ญํ ์ ํ์ผ ๋จ์๋ก ๋ถ๋ฆฌํด์.
๋ฌธ์ ์ํฉ
UI์ ๋ก์ง์ด ํ ํ์ผ์ ๋ค์์ฌ ๋จ์ผ ์ฑ ์ ์์น์ด ๋ฌด๋์ ธ์.
ํด๊ฒฐ๋ฐฉ๋ฒ
// โ
Container
export default function TodoContainer() {
const { text, setText, todos, addTodo } = useTodo();
return (
<TodoPresentation
text={text}
onTextChange={setText}
todos={todos}
onAdd={addTodo}
/>
);
}
// โ
Presentation
interface TodoPresentationProps {
text: string;
onTextChange: (text: string) => void;
todos: Array<{ id: string; text: string }>;
onAdd: () => void;
}
function TodoPresentation({ text, onTextChange, todos, onAdd }: TodoPresentationProps) {
return (
<>
<input value={text} onChange={(e) => onTextChange(e.target.value)} />
<button onClick={onAdd}>์ถ๊ฐ</button>
<ul>
{todos.map((t) => (
<li key={t.id}>{t.text}</li>
))}
</ul>
</>
);
}
์์ฆ์ Custom Hook์ด Container ์ญํ ์ ๋์ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์์. useTodo ๊ฐ์ ํ
์ด ๋ฐ์ดํฐ ๋ก์ง์ ๋ด๋นํ๊ณ Presentation ์ปดํฌ๋ํธ๋ ์์ํ๊ฒ UI๋ง ํํํ๋ ์์ด์์. Dan Abramov๋ 2019๋
์ "Custom Hook์ด ๊ฐ์ ๋ฌธ์ ๋ฅผ ๋ ์ ํด๊ฒฐํ๋ค"๊ณ ์ธ๊ธํ์ ๋งํผ ํจํด ์์ฒด์ ํ์์ฑ์ ์ค์์ง๋ง UI์ ๋ก์ง์ ๋๋๋ค๋ ๊ฐ๋
์ ์ฌ์ ํ ์ ํจํด์.
์ธ์ ์ฌ์ฉํ ๊น?
UI์ ๋ก์ง์ ๋๋ ๋์ผ๋ฉด ๊ฐ๋ ์ฑ๊ณผ ์ฌ์ฌ์ฉ์ฑ์ด ์ฌ๋ผ๊ฐ๋ ๊ฑด ์ฌ์ค์ด์์. ๋ค๋ง ์์ ํ๋ก์ ํธ์์๋ ์คํ๋ ค ํ์ผ์ด ๋ถํ์ํ๊ฒ ๋์ด๋๋ ๋๋์ด ๋ค ์ ์์ด์. ํ์๋ผ๋ฆฌ "์ด ์ ๋ ๊ท๋ชจ๋ฉด ๊ตณ์ด?"๋ผ๋ ๋ํ๋ฅผ ํ ๋ฒ์ฏค ๋๋ ๋ณด๋ ๊ฒ ์ข์์.
๊ทธ๋ฐ๋ฐ ์ฌ๊ธฐ์ ํ ๊ฐ์ง ์๋ฌธ์ด ์๊ฒจ์. UI์ ๋ก์ง์ ๋ถ๋ฆฌํ๋ ๊ฑด ์ข์๋ฐ ๊ทธ ๋ก์ง ์์ฒด๋ฅผ ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ์ฌ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์? ์๋ฅผ ๋ค์ด ์ธ์ฆ ์ฒดํฌ ๊ฐ์ ๊ณตํต ๊ธฐ๋ฅ์ ์ด ๊ตฐ๋ฐ ์ปดํฌ๋ํธ์ ๋ถ์ฌ์ผ ํ๋ ์ํฉ์ด๋ผ๋ฉด์.
๋ฌธ์ ์ํฉ
ํน์ ๊ธฐ๋ฅ์ ์ฌ๋ฌ ์ปดํฌ๋ํธ์ ์ฌ์ฌ์ฉํ๊ณ ์ถ์ ๋ ๋ถํ์ํ ๋ฐ๋ณต ์ฝ๋๊ฐ ์๊ฒจ์.
ํด๊ฒฐ๋ฐฉ๋ฒ
์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ๋ ๊ณ ์ฐจ ํจ์(HOC)๋ก ๊ธฐ๋ฅ์ ์ฃผ์ ํด์.
const checkAuthorization = () => {
// ๊ถํ์ ํ์ธํ๊ฑฐ๋ ๋ก์ปฌ์คํ ๋ฆฌ์ง๋ฅผ ํ์ธํด ์๊ฒฉ ์๋ฒ๋ก ์์ฒญ์ ๋ณด๋ด์.
};
const withAuthorization = <P extends object>(
Component: React.ComponentType<P>
) => {
return function WithAuthorization(props: P) {
const isAuthorized = checkAuthorization();
return isAuthorized ? <Component {...props} /> : <Login />;
};
};
์ค์ ๋ก ์ ์ฉํ ๋๋ ์ปดํฌ๋ํธ๋ฅผ HOC๋ก ๊ฐ์ผ ๊ฒฐ๊ณผ๋ฅผ ์ ๋ณ์์ ๋ด์์ ์จ์.
// โ
์ ์ฉ ์์
function Dashboard({ title }: { title: string }) {
return <h1>{title}</h1>;
}
// withAuthorization์ผ๋ก ๊ฐ์ธ๋ฉด ๊ถํ ์ฒดํฌ๊ฐ ์๋์ผ๋ก ๋ถ์ด์.
const ProtectedDashboard = withAuthorization(Dashboard);
// ์ฌ์ฉํ ๋๋ ์๋ณธ ์ปดํฌ๋ํธ์ ๋์ผํ props๋ฅผ ๊ทธ๋๋ก ๋๊ฒจ์.
<ProtectedDashboard title="๊ด๋ฆฌ์ ํ์ด์ง" />
์ธ์ ์ฌ์ฉํ ๊น?
์ค๋ณต ์ฝ๋๋ฅผ ์ ๊ฑฐํ๊ณ ์ธ์ฆ ๋ก์ง์ ํ ๊ณณ์์ ๊ด๋ฆฌํ ์ ์์ด์. ๋ฐ๋ฉด HOC๋ฅผ ์ฌ๋ฌ ๊ฐ ์ค์ฒฉํ๋ฉด ์ปดํฌ๋ํธ ํธ๋ฆฌ๊ฐ ๋ถํ์ํ๊ฒ ๊น์ด์ง๊ณ props ์ด๋ฆ์ด ์ถฉ๋ํ ๊ฐ๋ฅ์ฑ๋ ์์ด์. ์์ฆ์ Custom Hook์ด ์๋น ๋ถ๋ถ์ ๋์ฒดํ ์ ์์ด์ ์ ํ๋ก์ ํธ๋ผ๋ฉด HOC๋ถํฐ ๊บผ๋ด๋๋ ๊ฒฝ์ฐ๋ ๋ง์ง ์์ ํธ์ด์์.
HOC๊ฐ ์ปดํฌ๋ํธ ๋จ์์ ์ฌ์ฌ์ฉ์ ํด๊ฒฐํด์ค๋ค๋ฉด ์ํ ๊ด๋ฆฌ ์์ฒด๊ฐ ๋ณต์กํด์ง๋ ๋ฌธ์ ๋ ๋ณ๊ฐ์์. useState๊ฐ ๋ค์ฏ ๊ฐ์ฏค ์์ด๊ธฐ ์์ํ๋ฉด ์ด๋์ ๋ฌด์์ด ๋ฐ๋๋์ง ์ถ์ ํ๊ธฐ๊ฐ ์ ์ ์ด๋ ค์์ ธ์.
์ํ๊ฐ ๋ณต์กํด์ง๋ฉด useState๋ง์ผ๋ก๋ ํ๊ณ๊ฐ ๋๊ปด์ ธ์. ํนํ ์ฌ๋ฌ ์ํ๊ฐ ์๋ก ์ฝํ ์์ ๋ ์ด๋ค ์ด๋ฒคํธ๊ฐ ์ด๋ค ์ํ๋ฅผ ๋ฐ๊พธ๋์ง ํ์
ํ๊ธฐ ์ด๋ ต๊ณ , ์์์น ๋ชปํ ํ์ด๋ฐ์ ์ํ๊ฐ ํ์ด์ง๊ธฐ๋ ํด์.
useReducer๋ฅผ ์ฐ๋ฉด "์ด๋ค ์ผ์ด ์ผ์ด๋ฌ๋๊ฐ(action)"์ "๊ทธ ๊ฒฐ๊ณผ ์ํ๊ฐ ์ด๋ป๊ฒ ๋ฌ๋ผ์ง๋๊ฐ(reducer)"๋ฅผ ๋ช
ํํ ๋ถ๋ฆฌํ ์ ์์ด์.
interface Todo {
id: string;
text: string;
completed: boolean;
}
type Action =
| { type: "ADD"; text: string }
| { type: "TOGGLE"; id: string }
| { type: "DELETE"; id: string };
function todoReducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case "ADD":
return [
...state,
{
id: crypto.randomUUID(),
text: action.text,
completed: false,
},
];
case "TOGGLE":
return state.map((t) =>
t.id === action.id ? { ...t, completed: !t.completed } : t
);
case "DELETE":
return state.filter((t) => t.id !== action.id);
default:
const _exhaustiveCheck: never = action;
throw new Error(`Unknown action type`);
}
}
// โ
์ฌ์ฉ ์์
const [todos, dispatch] = useReducer(todoReducer, []);
dispatch({ type: "ADD", text: "์ ํ ์ผ" });
์๋ฅผ ๋ค์ด Action ํ์
์ { type: "CLEAR" }๋ฅผ ์ถ๊ฐํ๋๋ฐ case "CLEAR": ๋ถ๊ธฐ๋ฅผ ๋น ๋จ๋ฆฌ๋ฉด default์์ action์ด never๊ฐ ์๋ ํ์
์ผ๋ก ๋จ์ TypeScript ์ค๋ฅ๊ฐ ๋ฐ๋ก ๋ฐ์ํด์. ๋์น ์ก์
์ด ๋ฐํ์์์์ผ ํฐ์ง๋ ๊ฒ ์๋๋ผ ์๋ํฐ์์ ๋ฐ๋ก ์กํ๋ ๊ฑฐ์์.
์ํ ๊ด๋ฆฌ ๋ก์ง์ reducer ํ ๊ณณ์ ๋ชจ์๋๋ฉด "์ด ์ํ๊ฐ ์ ๋ฐ๋์์ง?"๋ผ๋ ์ง๋ฌธ์ ๋น ๋ฅด๊ฒ ๋ตํ ์ ์์ด์. ๋ค๋ง ์ํ๊ฐ ๋จ์ํ ๊ฒฝ์ฐ์๋ ์คํ๋ ค ์ฝ๋๊ฐ ์ฅํฉํด์ง๋๊น useState ๋์ธ ๊ฐ๋ก ์ถฉ๋ถํ ์ํฉ์์ ๊ตณ์ด ๊บผ๋ผ ํ์๋ ์์ด์.
์ํ ๊ด๋ฆฌ๊ฐ ์ ๋ฆฌ๋๋ค๋ฉด ์ด์ ์ปดํฌ๋ํธ ๊ฐ ์ํ ๊ณต์ ๋ฌธ์ ๋ก ๋์ด๊ฐ๋ณผ๊ฒ์. props๋ฅผ ๊น์ด ์ ๋ฌํ์ง ์์ผ๋ฉด์ ์ฌ๋ฌ ํ์ ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ ์ํ๋ฅผ ์ฐ๊ฒ ํ๋ ค๋ฉด ์ด๋ป๊ฒ ํด์ผ ํ ๊น์?
๋ฌธ์ ์ํฉ
Props drilling์ด ์ฌํด์ง๊ณ ์ต์ ์ด ๋ง์์ง์๋ก ์ปดํฌ๋ํธ ์ธํฐํ์ด์ค๊ฐ ๋ณต์กํด์ ธ์.
ํด๊ฒฐ๋ฐฉ๋ฒ
๋ถ๋ชจ ์ปดํฌ๋ํธ ์์์ ์์ ์ปดํฌ๋ํธ๋ค์ด Context๋ฅผ ํตํด ์ํ๋ฅผ ๊ณต์ ํ๊ฒ ํด์.
interface ToggleContextType {
on: boolean;
toggle: () => void;
}
const ToggleContext = createContext<ToggleContextType | null>(null);
function useToggleContext() {
const context = useContext(ToggleContext);
if (!context) throw new Error('Toggle ๋ด๋ถ์์๋ง ์ฌ์ฉ ๊ฐ๋ฅํฉ๋๋ค.');
return context;
}
function Toggle({ children }: { children: React.ReactNode }) {
const [on, setOn] = useState(false);
// toggle์ useCallback์ผ๋ก ์์ ํํ๊ณ value๋ฅผ useMemo๋ก ๋ฉ๋ชจ์ด์ ์ด์
ํด์.
// Context value๊ฐ ๋งค ๋ ๋๋ง๋ค ์ ๊ฐ์ฒด๋ก ๋ง๋ค์ด์ง๋ฉด
// ํ์ ์ปดํฌ๋ํธ ์ ์ฒด๊ฐ ๋ถํ์ํ๊ฒ ๋ฆฌ๋ ๋๋ ์ ์๊ฑฐ๋ ์.
const toggle = useCallback(() => setOn((o) => !o), []);
const value = useMemo(() => ({ on, toggle }), [on, toggle]);
return (
<ToggleContext.Provider value={value}>
{children}
</ToggleContext.Provider>
);
}
// Toggle.Button์ฒ๋ผ ์ปดํฌ๋ํธ์ ํ์ ์ปดํฌ๋ํธ๋ฅผ ํ๋กํผํฐ๋ก ๋ถ์ฌ์.
// ์ด๋ ๊ฒ ํ๋ฉด Toggle๊ณผ ๊ด๋ จ๋ ํ์ ์ปดํฌ๋ํธ๋ฅผ Toggle ๋ค์์คํ์ด์ค ์์์ ๊ด๋ฆฌํ ์ ์๊ณ
// ์ฌ์ฉ์ ์
์ฅ์์๋ "์ด ์ปดํฌ๋ํธ๋ Toggle ์์์ ์จ์ผ ํ๋ ๊ฒ"์์ ์ด๋ฆ๋ง ๋ณด๊ณ ์ ์ ์์ด์.
Toggle.Button = function ToggleButton() {
const { on, toggle } = useToggleContext();
return <button onClick={toggle}>{on ? "ON" : "OFF"}</button>;
};
Toggle.Message = function ToggleMessage({ children }: { children: React.ReactNode }) {
const { on } = useToggleContext();
return on ? <span>{children}</span> : null;
};
// โ
์ฌ์ฉ ์์
<Toggle>
<Toggle.Button />
<Toggle.Message>ํ ๊ธ์ด ์ผ์ก์ต๋๋ค!</Toggle.Message>
</Toggle>
์ธ์ ์ฌ์ฉํ ๊น?
ํ์ฅ์ฑ๊ณผ ๊ฐ๋ ์ฑ ๋ฉด์์ ํ์คํ ์ ์ฉํ ํจํด์ด์์. props drilling ์ง์ฅ์ ํผํ๋ฉด์ ์ปดํฌ๋ํธ๋ฅผ ์กฐ๋ฆฝํ๋ฏ ์ธ ์ ์์ผ๋๊น์. ์ ์ญ ์ํ๊ฐ ํ์ํ ์์ค์ด ์๋๋ผ ํน์ UI ๋ธ๋ก ์์์๋ง ๊ณต์ ๋๋ ์ํ๋ผ๋ฉด ์ด ํจํด์ด ๋ฑ ๋ง์์.
์ด์ฏค ๋๋ฉด ํ ๊ฐ์ง ๊ณตํต์ ์ด ๋ณด์ฌ์. ์์ ์๊ฐํ ํจํด๋ค, HOC๋ Compound Components๋ ๊ฒฐ๊ตญ "๋ก์ง์ ์ด๋์ ๋๊ณ ์ด๋ป๊ฒ ์ฌ์ฌ์ฉํ ๊ฒ์ธ๊ฐ"๋ผ๋ ์ง๋ฌธ์์ ์ถ๋ฐํ์ด์. ๊ทธ๋ฆฌ๊ณ ํ๋ React์์ ์ด ์ง๋ฌธ์ ๊ฐ์ฅ ์ง์ ์ ์ผ๋ก ๋ตํ๋ ์๋จ์ด ๋ฐ๋ก Custom Hook์ด์์.
์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๋๊ฐ์ ํผ ๊ด๋ฆฌ ์ฝ๋๋ฅผ ๋ณต๋ถํ๋ค๊ฐ ์ง์น ์ ์์ด์? before/after๋ฅผ ๋ณด๋ฉด ์ด๊ฒ ์ ํ์ํ์ง ๋ฐ๋ก ๋๊ปด์ ธ์.
// โ Before: ์ปดํฌ๋ํธ๋ง๋ค ๊ฐ์ ์ฝ๋๊ฐ ๋ฐ๋ณต๋ผ์.
function LoginForm() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
return (
<form>
<input value={username} onChange={(e) => setUsername(e.target.value)} />
<input value={password} onChange={(e) => setPassword(e.target.value)} />
</form>
);
}
// โ
After: ๋ฐ๋ณต ๋ก์ง์ ํ
์ผ๋ก ๋ฝ์๋ด๋ฉด ์ปดํฌ๋ํธ๊ฐ ๊น๋ํด์ ธ์.
function useFormInput(initial = "") {
const [value, setValue] = useState(initial);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
};
const reset = () => setValue(initial);
// inputProps๋ง ๋ฐ๋ก ๋ฌถ์ด์ ๋ฐํํด์.
// { value, onChange, reset }์ ํต์งธ๋ก ์คํ๋ ๋ํ๋ฉด
// reset์ด DOM์ ์ ์ ์๋ ์์ฑ์ผ๋ก ์ ๋ฌ๋์ด ์ฝ์ ๊ฒฝ๊ณ ๊ฐ ๋ฐ์ํ๊ฑฐ๋ ์.
return { inputProps: { value, onChange }, reset };
}
function LoginForm() {
const username = useFormInput("");
const password = useFormInput("");
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
await login(username.inputProps.value, password.inputProps.value);
};
return (
<form onSubmit={handleSubmit}>
<input {...username.inputProps} placeholder="์์ด๋" />
<input {...password.inputProps} type="password" placeholder="๋น๋ฐ๋ฒํธ" />
<button type="submit">๋ก๊ทธ์ธ</button>
</form>
);
}
์ด์ ์ HOC๋ Render Props๊ฐ ๋ด๋นํ๋ ์ญํ ๋๋ถ๋ถ์ Custom Hook์ด ๋์ฒดํ๋ฉด์ ํ๋ React์์ ๊ฐ์ฅ ์์ฃผ ์ฐ์ด๋ ํจํด์ด ๋์ด์. ๋ก์ง ์ฌ์ฌ์ฉ์ด๋ผ๋ ๋ชฉ์ ๋ง ๋ณด๋ฉด ๊ฑฐ์ ๋ง๋ฅ์ ๊ฐ๊น์ฃ . ๋ค๋ง ํ๊ณ๋ ์์ด์. ๋ก์ง์ ๋ถ๋ฆฌํ ์ ์์ง๋ง ์ฌ๋ฌ ์ปดํฌ๋ํธ ์ฌ์ด์ UI ์ํ ๊ณต์ ๋ ์ฌ์ ํ ๋ณ๋ ์๋จ(Context, ์ํ ๊ด๋ฆฌ ๋ผ์ด๋ธ๋ฌ๋ฆฌ)์ด ํ์ํด์. ํ
์ ํ
์คํธํ๋ ค๋ฉด renderHook ๊ฐ์ ํ
์คํธ ํ๊ฒฝ ์ค์ ๋ ๋ฐ๋ก ์ฑ๊ฒจ์ผ ํ๊ณ ์.
์ธ์ ์ฌ์ฉํ ๊น?
๋์์ธ ํจํด์ ๋ง๋ฅ ํด๊ฒฐ์ฑ
์ ์๋๋ผ๊ณ ์๊ฐํด์.
ํ์ง๋ง ๋ฐ๋ณต๋๋ ๋ฌธ์ ์ ๋ํ ๊ฒ์ฆ๋ ํด๋ฒ์ด๋ผ๊ณ ๋ณผ ์ ์์ด์.
React์์ ์์ฃผ ์ฐ์ด๋ ํจํด๋ค์ ์ดํดํ๊ณ ์ ์ ํ๊ฒ ํ์ฉํ๋ฉด ์ฝ๋ ํ์ง์ด ๋์ ๋๊ฒ ์ข์์ง ์ ์์ด์.