현재 진행하고 있는 사이드 프로젝트에 pr을 요청했더니 제목과 같은 요청이 왔다. 먼저 해당 코드부터 보자.
// import 생략
const Calender = () => {
const [currentMonth, setCurrentMonth] = useState<Date>(new Date());
const [selectedDate, setSelectedDate] = useState<string>("");
// 바로 이 부분...
const calendarDays = useMemo(
// 컴포넌트에서 선언하기 전에 getCalendarDays함수를 호출하고 있다!
() => getCalendarDays(currentMonth),
[currentMonth]
);
// ...
}
// 함수는 나중에 선언되었다.
function getCalendarDays(currentMonth: Date) {
// ...
return currentDays;
}
// styled-components 생략
물론 함수 선언문을 사용한 것에는 이유가 있다. 지난번 프로젝트에서 코드의 가독성이 지나치게 떨어졌기 때문에 이번에는 렌더링을 위한 코드와 로직을 위한 (값을 연산하기 위한) 코드를 분리하려 시도한 것이다.
위와 같이 코딩하면 바라건대 내가 적은 컴포넌트를 처음 보는 사람도 다음과 같은 순서로 접근할 수 있을 것이다.
다만 이 순서대로 접근하려면 호이스팅이 일어나야 한다.
호이스팅으로 인해 발생할 수 있는 오류를 방지하기 위한 금언이다.
하지만 컴포넌트 내부에서 로직을 위한 함수를 차례대로 표현식으로 정의하다보면 가독성에 문제가 생긴다.
로직을 활용하기 위한 컴포넌트는 저 아래 어딘가에 위치하게 되는 것이다. 가뜩이나 styled-components와 관련된 코드를 아래쪽에서 선언하고 있는데 표현식으로 정의하면 실제 컴포넌트를 발견할 때까지 스크롤해야 할 것이다. 나는 이런 의도를 열심히 설명했다.
이런 내용을 설명하면서 나는 getCalendarDays함수를 표현식으로 변형했다. 당연히 레퍼런스 에러를 기대하면서, 나는 팀원에게 화면을 공유했다. 그리고 화면에는 멀쩡하게 그려진 캘린더가 표시되어있었다. 마치 함수 표현식의 호이스팅이 선언문처럼 발생한 것 같은 상황이었다.
내 지식으로는 이해할 수가 없어서 몇가지 테스트를 진행해봤다.
위의 테스트 결과를 볼 때 함수 표현식을 정의하기 전에 호출할 수 있는 것은 분명히 언어단에서 의도한 동작이 아니다. (eslint와 타입스크립트 자체적으로도 이를 막기 위한 규칙이 있다.)
바벨로 트랜스하는 과정에서 의도치 않게 이런 일이 발생하는 걸까?
아직 바벨에 대해서는 이해가 미흡하기 때문에 계속 추적할 수 있도록 기록해두고 현재 환경에서 에러를 발생시키려 시도해 보았다.
재현 환경
- yarn create react-app my-app --template typescript로 프로젝트 생성
중첩 함수에서는 내부에서는 또 해당 방식으로 동작하지 않는다. 타입스크립트는 비로소 여기에서 에러를 토해냈다.
const getCalendarDays = (currentMonth: Date) => {
const monthStartDate = startOfMonth(currentMonth);
const monthEndDate = endOfMonth(monthStartDate);
const calendarStartDate = startOfWeek(monthStartDate);
const calendarEndDate = endOfWeek(monthEndDate);
const prevMonthDays = getPrevDays();
const currentMonthDays = getCurrentDays();
const nextMonthDays = getNextDays();
const calendarDays = prevMonthDays.concat(
currentMonthDays.concat(nextMonthDays)
);
return calendarDays;
// 아래의 세 중첩 함수를 표현식으로 전환하면 비로소 타입스크립트가 에러를 발생시킨다.
function getPrevDays() {
const prevLastDay = format(endOfMonth(subMonths(currentMonth, 1)), "d");
const monthFirstDay = format(calendarStartDate, "d");
if (format(monthStartDate, "d") === format(calendarStartDate, "d"))
return [];
const prevDays = Array.from(
{
length: parseInt(prevLastDay, 10) - parseInt(monthFirstDay, 10) + 1,
},
(undef, daynumber) => ({
id: crypto.randomUUID(),
day: parseInt(monthFirstDay, 10) + daynumber + 1,
isValid: false,
})
);
return prevDays;
}
function getCurrentDays() {
const MonthLastDay = format(monthEndDate, "d");
const currentDays = Array.from(
{
length: parseInt(MonthLastDay, 10),
},
(undef, dayNumber) => ({
id: crypto.randomUUID(),
day: dayNumber + 1,
isValid: true,
})
);
return currentDays;
}
function getNextDays() {
const calendarEndDay = format(calendarEndDate, "d");
if (format(monthEndDate, "d") === format(calendarEndDate, "d")) return [];
const nextDays = Array.from(
{
length: parseInt(calendarEndDay, 10),
},
(undef, daynumber) => ({
id: crypto.randomUUID(),
day: daynumber + 1,
isValid: false,
})
);
return nextDays;
}
}