개발새발
React 직접 타이핑 하기 본문
1. 프로젝트 구조와 준비
src/
├─ zeact.tsx ← 우리가 직접 작성할 타입 선언 파일
└─ WordRelay.tsx
- zeact.tsx : declare namespace Zeact { … } 블록 안에 모든 타입·함수를 선언한다.
- WordRelay.tsx : 원래 React 예제를 그대로 두되, React. 대신 Zeact. 접두어를 붙여 사용한다.
타입스크립트 컴파일러는 같은 루트(혹은 tsconfig.json의 "include")에 있는 .ts·.tsx 파일을 모두 스캔하므로, 따로 types 경로를 지정할 필요는 없다.
2. 훅(Hooks) 선언하기
2-1. 가장 단순한 형태
declare namespace Zeact {
const useState: () => void;
const useRef: () => void;
const useEffect: (cb: Function) => void;
const useCallback: (cb: Function) => void;
}
이렇게 시작하면 모든 호출부가 any 타입으로 추론되어 오류가 쏟아진다. 의도는 “타입 안전” 훈련이므로, 훅마다 제네릭을 줘야 한다.
2-2. useState · useRef의 제네릭 버전
declare namespace Zeact {
const useState: <T>(initial: T) => [T, (v: T) => void];
const useRef: <T>(initial: T | null) => { current: T | null };
}
- useState<'hello'> → [string, (string) => void]
- useRef<HTMLInputElement> → { current: HTMLInputElement | null }
제네릭을 쓰면 모든 타입(문자열, 숫자, 객체, DOM 노드 …)에 대해 재사용 가능하다.
2-3. 의존성 배열을 받는 훅
declare namespace Zeact {
const useEffect: (cb: Function, deps: unknown[]) => void;
const useCallback: <F extends Function>(cb: F, deps: unknown[]) => F;
}
- deps에 unknown[]을 주어 아무 배열이나 통과시킬 수 있게 했다.
- useCallback은 넘겨받은 함수 그대로를 다시 반환하므로 return 타입을 F로 지정한다.
3. 이벤트 객체 타입 만들기
React 원본은 SyntheticEvent 계층을 두지만, 여기서는 최소한만 구현한다.
declare namespace Zeact {
interface FormEvent<T = Element> {
preventDefault(): void;
// 필요하면 currentTarget, target 등을 추가
}
interface ChangeEvent<T = Element> {
currentTarget: T;
}
}
- 제네릭 T가 어떤 DOM 요소인지 알려 주며, 호출부(예: ChangeEvent<HTMLInputElement>)에서 명확한 타입으로 바인딩된다.
4. 컴포넌트 타입 설계
4-1. 함수 컴포넌트(극단 축약본)
declare namespace Zeact {
// P = props
interface FunctionComponent<P = {}> {
(props: P): JSX.Element;
}
}
- 실제 React에서는 propTypes, defaultProps, contextTypes 같은 정적 필드를 포함하지만, 학습 목적이므로 반환형만 지정했다.
4-2. Zeact.ReactNode
React의 ReactNode는 문자열·숫자부터 포털까지 감싸는 거대한 유니언이다.
여기에선 단순화를 위해 원본 타입을 그대로 재사용하자.
type ReactNode = React.ReactNode;
5. Form 컴포넌트 예제
// Props 선언
interface Props {
children: Zeact.ReactNode;
onSubmit: (e: Zeact.FormEvent<HTMLFormElement>) => void;
}
// 컴포넌트 구현
const Form: Zeact.FunctionComponent<Props> = ({ children, onSubmit }) => (
<form onSubmit={onSubmit}>{children}</form>
);
- children은 반드시 Zeact.ReactNode여야 한다.
() => JSX.Element처럼 함수 타입을 쓰면 JSX 내부에 그냥 함수 자체가 들어가는 꼴이 되어 버린다. - onSubmit은 우리가 정의한 FormEvent를 사용한다.
6. 최종 WordRelay 코드 요약
const WordRelay = () => {
const [word, setWord] = Zeact.useState('제로초');
const [value, setValue] = Zeact.useState('');
const [result, setResult] = Zeact.useState('');
const inputEl = Zeact.useRef<HTMLInputElement>(null);
Zeact.useEffect(() => console.log('useEffect 실행'), []);
const onSubmitForm = Zeact.useCallback(
(e: Zeact.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const input = inputEl.current;
if (word[word.length - 1] === value[0]) {
setResult('딩동댕');
setWord(value);
} else {
setResult('땡');
}
setValue('');
input?.focus();
},
[word, value],
);
const onChange = Zeact.useCallback(
(e: Zeact.ChangeEvent<HTMLInputElement>) =>
setValue(e.currentTarget.value),
[],
);
return (
<>
<div>{word}</div>
<Form onSubmit={onSubmitForm}>
<input ref={inputEl} value={value} onChange={onChange} />
<button>입력!</button>
</Form>
<div>{result}</div>
</>
);
};
export default WordRelay;
컴파일 오류가 0이라면, 우리가 작성한 zeact.tsx의 타입 선언만으로도 충분히 타입 추론이 되었다는 뜻이다.
7. 연습 팁
- 처음부터 다시 useState, useRef를 타입 없이 선언한 후, 오류 메시지를 보고 한 단계씩 제네릭·반환형을 채워 넣어 보자.
- useReducer, memo, forwardRef 등을 직접 타이핑해 보는 것도 유익하다.
- 익숙해지면 Zeact가 아닌 실제 React 선언 파일을 열어 보고, “왜 이런 제약 조건을 걸었을까?”를 추적해 보자.
결국 핵심은 “타입스크립트는 설계 문서다.”
훅과 컴포넌트를 어진욧 케이스로 좁히면, 최소 타입만으로도 원하는 수준의 타입 안정성을 달성할 수 있다는 점을 체험해 보자!
'Typescript' 카테고리의 다른 글
js 파일 생성하기 (0) | 2025.05.30 |
---|---|
JSX 타입 이해하기 (0) | 2025.05.24 |
React Hooks 분석하기 (0) | 2025.05.24 |
React 타입 분석하기 (1) | 2025.05.24 |
axios의 타입을 어떻게 찾았는지 이해하기 (0) | 2025.05.17 |