들어가며
토스에서 Frontend Fundamentals 모의고사 2회차를 진행했다. 3일 동안 과제를 진행해 제출하고, 해설 강의를 진행하는 방식이다. 결론부터 말하면, 이 강의를 보고 내 리팩토링 방식이 바뀌었다. 해설 강의에서 배운 것의 핵심과 느낀 점, 그리고 과제를 리팩토링한 과정을 남긴다.
해설 강의 - 핵심
- 인터페이스 설계가 매우 중요하다.
UI를 바탕으로 인터페이스를 “직접” 먼저 설계한다. AI에게 리팩토링 또한 맡겨버리게 되면, 내가 원하는, 적절한 인터페이스를 설계하지 못하게 된다. 만약 AI를 활용한다면 인터페이스를 직접 설계하고, 이를 AI에게 전달해서 리팩토링을 맡기자.
- 코드는 읽는 게 아니라 예측하는 것이다.
함수의 인터페이스를 볼 때 “이런 역할을 하겠구나”라고 예측이 되는 코드가 좋은 코드이다. 인간은 한 번에 6-7개의 기억밖에 하지 못하며, 예측이 가능해야 효율적으로 이 기억을 활용할 수 있다.
추상화 수준을 맞추는 것도 이런 기억할 개수를 줄일 수 있는 좋은 방법이다.
- 개발자에게 가장 중요한 자질은 논리적 사고와 비판적 사고이다.
내가 선택한 기술, 사용한 아키텍처, 작성한 코드에 대해 근거를 이야기할 수 있는 논리적 사고와, ‘더 나은 기술, 아키텍처, 코드는 없을까?’에 대해서 의문을 품는 비판적 사고.
다른 사람의 말은 사고를 확장하는 데에 써야지 ‘이게 정답이다’라고 생각해서는 안 된다. 여기서도 중요한 건 비판적 사고.
- ‘AI로 생산성이 높아졌다’고들 이야기하는데, 우리는 정작 ‘생산성’에 대한 정의조차 내리지 않은 경우가 많다.
AI를 썼을 때 생산성이 무조건 높아졌다면, 1명이 하던 일을 3명 몫으로 할 수 있어야 하지만 현실은 그렇지 않다.
느낀 점
“개발자에게 가장 중요한 자질은 논리적 사고와 비판적 사고이다.”
“AI시대에 내가 개발자로서 무엇에 집중해야 할까”라는 고민을 요즘 들어 자주 하고는 했다. 이에 대한 좋은 답이라 생각했다. UI 퍼블리싱, AI가 더 잘해준다. 기능 구현, AI가 오히려 능숙하다. 그런데 이 코드를 유지보수하고, 오류가 발생하면 명확한 원인을 찾아서 해결하는 건 결국 인간이 해야 할 몫이다. 여기서 코드를 유지보수한다는 것은 기존 코드베이스에서 기존 기능을 수정하거나, 새로운 기능을 추가하는 것을 의미한다. 이 때 기존 코드가 복잡해서 읽기 힘들고, 특정 기능이 어디에 있는지 찾기 힘들다면 이는 유지보수하기 어려운 코드임을 의미한다. 이 또한 AI에게 맡기면 안 되느냐라고 이야기할 수 있지만, 내가 의도한 기능이 맞는지, 복잡한 요구사항이 빠짐없이 반영됐는지, 다른 코드에 영향은 없는지. 이 판단은 결국 인간이 해야 한다. 이 코드를 책임지는건 결국 코드를 작성한 AI가 아니라, 이 코드로 사용자에게 서비스를 제공하는 개발자이기 때문이다. 오류가 발생하면 명확한 원인을 찾아서 해결하는 것 또한 비슷한 맥락이다. AI에게 버그 해결을 맡기면 디자인 수정, 기능의 엣지케이스 처리와 같은 단순한 버그 해결은 AI가 잘해줄 수 있다. 하지만 좀 더 복잡한 버그라면? 모노레포에서 특정 패키지가 빌드가 안되는데 이러한 버그 해결을 AI에게 맡긴다면? AI가 우연히 이 버그를 해결했다고 할지라도 명확한 원인을 찾아서 이를 해결했다는 걸 어떻게 알 수 있을까? 이 또한 결국 인간이 해야할 몫이다.
- 특히, AI를 최대한 적극적으로 쓴 요즘 느끼는 건 “AI의 응답은 지난 문자들을 기반으로 만들어낸 그럴듯한 결과물”이라는 것이다. 이것이 LLM이 극복하지 못한 한계이기도 하다. LLM은 ‘사고’를 하지 못한다. 그저 빅데이터로 만들어낸 결과물에 불과하다.
정리하면 퍼블리싱, 기능 개발과 같이 AI가 확연히 잘하는 분야가 있다. 하지만 개발자가 계속해서 해야할 유지보수, 버그 해결이 있다는 것은 분명하다. 그래서 앞으로 뭘 갖춰야 하는지도 뚜렷해진다. 위에서 이야기한 논리적 사고와 비판적 사고, 설계 역량, 동작 원리 등 깊이있는 지식. 논리적 사고가 있어야 어떤 방식이 더 나은 방식인지 근거를 기반으로 사고를 해 결정을 내릴 수 있다. 그리고 비판적 사고가 있어야 AI가 작성한 코드와 구현한 기능이 비즈니스 요구사항을 고려해 기능을 명확하게 구현했는지, 엣지 케이스는 없는지를 검증할 수 있는 것이다.
AI를 썼을 때 생산성이 무조건 높아졌다면, 1명이 하던 일을 3명 몫으로 할 수 있어야 하지만 현실은 그렇지 않다.
따라서 AI로 인해서 우리의 생산성이 무조건 ‘높아졌다’고 하기 힘들다. AI 덕분에 퍼블리싱, 단순 기능 개발의 영역에서는 빨라졌지만, 이 코드를 추후 유지보수를 위해 더 나은 설계 방식으로 바꾸고, 빠뜨린 기능을 체크하고, AI가 작성한 코드에서 발생한 오류를 해결하는 데에 그만큼의 시간을 다시 투자해야하기 때문이다.
개발자에게 중요한 역량이 조금 더 근본적인 역량으로 바뀌었다. 특히 ‘설계 역량’. 1번에서 이야기한 ‘인터페이스 설계 역량’이 이에 해당한다. 새로 생긴 역량은 아니다. 원래 중요했던 게 더 중요해졌을 뿐이다. 이전에는 프론트엔드 개발자에게 퍼블리싱, 기능 구현의 역량과 함께 설계 역량도 중요했지만, 이제는 설계 역량 쪽에 더 중요도가 옮겨간 것이다.
인터페이스 설계가 매우 중요하다.
이번 해설 강의에서 가장 인상깊었던 건 AI가 작성한 코드를 뒤로 하고, UI를 보며 인터페이스를 설계하는 것이었다. 이 영상을 보고 머리를 한 대 맞은 느낌이었고, 앞으로 이런 방식으로 개발을 해봐야겠다는 생각을 했다. 나는 지금껏 리팩토링을 할 때 AI가 작성해준 코드를 바탕으로 이런이런 단위로 컴포넌트를 묶어줘 라는 식으로 지시를 하고는 했다. AI가 작성한 코드를 보며 설계를 한 탓에 AI가 작성한 코드의 틀을 벗어날 수 없었다. 하지만 AI가 작성한 코드가 아니라, UI를 바탕으로 인터페이스를 먼저 설계를 하면 내 의도에 따라서 설계를 할 수 있겠다는 생각이 들었다.
그리고 앞으로는 단순히 기능 개발 뿐 아니라, ‘인터페이스 설계’에 집중을 해봐야겠다고 생각을 했다. 인터페이스를 잘 설계한다는 것은 결국 함수 등 추상화에 대한 인지 부하를 줄이는 것을 의미하고, 이는 곧 가독성 개선을 의미한다. 인터페이스를 잘 설계하면 ‘이 함수가 이 역할을 하는구나’ 하는 예측 가능성이 높아지고, 이로 인해 우리가 더 신경 써야 할 것들이 줄어든다. 하지만 인터페이스를 잘못 설계하면 ‘이 함수는 이름은 이거지만, 내부적으로 이런 역할도 하는구나’를 하나 더 인지해야 한다. 그리고 인터페이스를 잘 설계해 추상화에 대한 인지 부하를 줄이면, 상대적으로 더 많은 정보를 기억하고 있기 때문에 더 코드를 잘 읽을 수 있게 된다.
이런 인터페이스 설계를 개발 프로세스에 넣는다면 이렇게 되지 않을까?
- 기획과 피그마 디자인을 확인한다.
- 비즈니스 요구사항을 바탕으로 신경써야할 것, 빠뜨린 내용 없는지 체크한다.
- 인터페이스를 설계한다.
- UI에 맞게 컴포넌트를 설계한다.
- 함께 고민해볼 것
- 상태 관리를 어디서 할 지(클라이언트 상태, Context API, 전역 상태, search params, localStorage 등)
- 데이터 패칭을 어디서 할 지
- 함께 고민해볼 것
- UI에 맞게 컴포넌트를 설계한다.
- AI에게 기능 구현을 맡긴다.
- 검토한다.
- 내가 설계한 대로 구현했는지, 핵심 기능과 엣지케이스를 고려해서 개발했는지 검토한다.
해설 강의 후 리팩토링
export function ReservationStatusPage() {
const navigate = useNavigate();
const location = useLocation();
const [date, setDate] = useState(() => formatDate(new Date()));
const [activeReservation, setActiveReservation] = useState<string | null>(null);
const locationState = location.state as { message?: string } | null;
const [message, setMessage] = useState<Message | null>(
locationState?.message ? { type: 'success', text: locationState.message } : null
);
useEffect(() => {
if (locationState?.message) {
window.history.replaceState({}, '');
}
}, [locationState]);
const { data: rooms } = useRooms();
const { data: reservations } = useReservations(date);
const { data: myReservationList } = useMyReservations();
const cancelMutation = useCancelReservation();
const handleCancel = async (id: string) => {
try {
await cancelMutation.mutateAsync(id);
setMessage({ type: 'success', text: MESSAGES.CANCEL.SUCCESS });
} catch {
setMessage({ type: 'error', text: MESSAGES.CANCEL.FAILURE });
}
};
const getRoomName = (roomId: string) => rooms.find((room: Room) => room.id === roomId)?.name ?? roomId;
return (
<div
css={css`
background: ${colors.white};
padding-bottom: 40px;
`}
>
<Top.Top03
css={css`
padding-left: 24px;
padding-right: 24px;
`}
>
회의실 예약
</Top.Top03>
<Spacing size={24} />
<DateSelector date={date} onDateChange={setDate} />
<Spacing size={24} />
<Border size={8} />
<Spacing size={24} />
{/* .. */}
)
}
export function ReservationStatusPage() {
return (
<DateProvider>
<main>
<ReservationHeader />
<Spacing size={24} />
<DateSelector />
<Spacing size={24} />
<Border size={8} />
<Spacing size={24} />
<ReservationTimeline />
<Spacing size={24} />
<Border size={8} />
<Spacing size={24} />
<LocationMessageBanner />
<MyReservationList />
<Spacing size={24} />
<Border size={8} />
<Spacing size={24} />
<BookingButton />
<Spacing size={24} />
</main>
</DateProvider>
);
}
- 기존 코드의 문제점
- 페이지 컴포넌트의 책임이 너무 무겁다.
- 여러 관심사(페이지 라우팅, 메시지, 데이터 패칭)가 한 컴포넌트에 혼재되어 있다.
- 한 파일에 너무 많은 정보가 담겨있다 → 인지 부하 → 가독성 저하
- 한 컴포넌트 안에서 추상화 수준이 서로 맞지 않는다. (어떤 건 저수준 DOM 로직, 어떤 건 고수준 컴포넌트가 뒤섞여 있다)
- 페이지 컴포넌트의 책임이 너무 무겁다.
- 해결 방법
- 각 컴포넌트에 따라 관심사를 분리했다.
- 추상화 수준을 맞췄다. 페이지 레벨에서는 모두 같은 높이의 컴포넌트로 추상화하고, 각 컴포넌트 내부는 그 이상 더 쪼개지 않았다.
사실 이 리팩토링에서 가장 고민했던 건 DateProvider였다. before 코드를 보면 date 상태가 페이지 컴포넌트 최상단에 useState로 박혀있고, 이 값을 DateSelector, useReservations(date) 등 여기저기서 끌어다 쓰고 있었다. 그러다 보니 페이지 컴포넌트가 date를 쥐고 있느라 통째로 무거워진 것이다. 여기서 앞에서 이야기한 개발 프로세스의 ‘상태 관리를 어디서 할 지’ 단계가 그대로 걸렸다. date는 이 화면 여러 컴포넌트가 공유하지만 서버 상태도, 전역 상태도 아닌 딱 이 화면 안에서만 도는 값이다. 그래서 Context API로 빼는 게 맞다고 판단했고, DateProvider로 감싸서 date를 필요로 하는 컴포넌트들이 각자 알아서 꺼내 쓰도록 했다. date를 어디서 관리할지 먼저 정하고 나니 페이지 컴포넌트에 남는 게 없었다. 각 컴포넌트가 자기가 쓸 값을 알아서 꺼내 쓰기 때문이다.
마치며
‘토스에서는 코드를 작성할 때 무엇을 중요하게 생각하는지’알게 된 값진 시간이었다. 그리고 나는 앞으로 어떤 식으로 코드를 작성하고 개발을 해나가야 할지 방향성을 고민해볼 수 있는 시간이었다.
앞으로 AI에게 코드 작성을 전부 맡기고 기능 검사만 하는 개발자가 아니라, 추후 이 코드를 다시 보게될 개발자를 위해(내가 될 수도 있고), 기능을 보다 빠르게 수정하고 추가해서 사용자들에게 제공하기 위해서라도 코드를 잘 설계하고 검토할 수 있는 개발자가 되어야겠다고 생각했다. 특히 해설 강의에서 알려준 ‘인터페이스 설계’를 접목시켜봐야겠다.
‘코드에는 정답이 없다. 그렇기에 코드에 대해서 토론을 하는 것이 즐거운 것이다‘ 최근에 커피챗을 하며 인상 깊었던 이야기이다. 이번 Frontend Fundamentals 모의고사 2회차를 바탕으로 스터디에서도 이야기를 나눴는데 짧은 시간이었지만 너무 즐거웠다. 이전에는 ‘정답이 있다’고 생각해 이 정답을 찾으려 애썼다면, 이번에는 ‘근거 있는 주장’을 이야기하는데에 초점을 맞추었더니 더 대화가 풍부해지고 대화가 즐거워졌다. 앞으로도 코드에 대해서 더 많은 이야기를 해보고싶다. 이런 스터디를 하나 만들어도 좋을 듯하다.
오랜만에 공개된 글을 작성하니 되게 시간이 가는 줄 모르고 내 생각들을 적었다. 취준을 시작한 이후에 공개된 글을 작성하는 빈도가 줄어들었는데, 앞으로는 좀 더 자주 써야겠다는 생각을 했다. 재밌었다.