레거시와 소통하기 : 제이쿼리와 리액트가 공존하는 길

레거시(legacy)는 네이버 영어사전에 ‘(죽은 사람이 남긴) 유산, (과거의) 유산’으로 등록이 되어 있습니다. ‘과거의 유산’이라는 정의에 코드를 빗대어 보면, 어제의 내가 짠 코드는 오늘의 나에게 ‘레거시 코드’라고 할 수 있겠네요.

이 글에서 다룰 레거시 코드는 단순히 시간만 지나온 코드는 아닙니다. 구름IDE의 제이쿼리(jQuery) 코드는 6~7년이 지났을 만큼 오래되고, 최근 도입한 기술 스택과도 다르며, 그 누구도 인수인계 받은 적이 없는 코드들입니다.

서비스 곳곳에서 여전히 동작하고 있는 이 제이쿼리 코드에는 몇 가지 문제가 있었습니다.

  1. 필자인 저를 포함하여 현재 팀원들은 제이쿼리를 제대로 사용해본 적이 없습니다.
  2. 어떤 액션에 대한 메소드가 엄청나게 깊은 곳에 숨겨져 있어서 찾기 힘듭니다.
  3. 테스트 코드가 없어 수정 시 어떤 ‘사이드 이펙트(side effect, 의도하지 않은 결과)’를 가져올지 모릅니다.

이러한 문제를 해결하기 위해 리팩터링으로 제이쿼리 코드 자체를 리액트(React)와 JS로 옮기는 작업을 진행하고 있습니다. 하지만, 작업 속도는 느렸고, 리팩토링이 끝났더라도 고객의 눈에는 바뀐 것이 없어, 제품이 개선없이 방치되고 있다는 인상을 줄 수 있었습니다.

리팩터링이 끝까기만을 무작정 기다릴 수 없었습니다. 리팩터링과 더불어 UI/UX에 변화를 주는 작업을 병행해야만 했습니다. 그러려면 제이쿼리와 리액트가 공존할 방안을 찾아야만 했습니다.

이 글은 제이쿼리와 리액트가 서로 소통하며 공존하는 두 가지 방안에 대한 이야기입니다. 커스텀 이벤트 도입과 모달 생성 방식 자체를 바꾼 이야기죠. 서로 다른 두 기술 스택이 어떻게 공존할 수 있었는지를 지금부터 들려드리겠습니다.

written by wynter
edit by snow

1. 커스텀 이벤트로 소통하기

자바스크립트(JavaScript, 이하 JS)에는 이벤트라는 개념이 있습니다. mdn은 ‘이벤트’란 개념을 다음과 같이 소개합니다.

Events are things that happen in the system you are programming, which the system tells you about so your code can react to them.

이벤트(event)란 여러분이 프로그래밍하고 있는 시스템에서 일어나는 사건(action) 혹은 일어난 일(occurrence)입니다. 시스템이 이벤트 발생을 알려주면 이벤트를 어떻게 처리할지를 코드로 정의해 처리할 수 있습니다.

이벤트로는 마우스 이벤트, 키 이벤트, 폼 이벤트 등이 있습니다. 대표적인 이벤트 유형으로는 click, keydown, focus 등이 있습니다. JS에서 제공하는 이벤트 말고도 사용자가 직접 이벤트를 만들 수 있는데, 이를 ‘커스텀 이벤트’라고 합니다.

먼저 이벤트 리스너는 다음과 같이 등록합니다. 레거시 코드의 메소드를 이용하기 때문에 레거시 코드에 이벤트를 등록합니다.

window.addEventListener('드래그시작', () => {
	document.querySelectorAll('.drag-ui').forEach((dragElement) => {
		const dropHelperEl = self.createDropHelper();
		dragElement.insertBefore(dropHelperEl, dragElement.firstChild);
  })
})

이벤트 발생 코드는 다음과 같습니다.

const dragStart = () => {
	// ...
	window.dispatchEvent(new CustomEvent('드래그시작'));
}

커스텀 이벤트를 사용하면 뭐가 좋냐고요? 지금껏 그래왔듯 리팩토링을 하면, 코드 변경점이 엄청 많습니다. 레거시 로직을 모두 다 옮기지 않았지만 리뷰하기 힘든 양이 쌓였을 정도죠.

그림_ 리팩터링으로 인한 코드 변경점들

커스텀 이벤트의 장점은 모든 레거시 코드를 옮겨오거나 바꿀 필요가 없다는 점입니다. 레거시와 엮인 모든 부분을 풀기엔 너무 방대할 때, 커스텀 이벤트를 이용하면 기존 레거시 로직을 적절히 재사용할 수 있습니다. 기존에 작동하고 있던 로직을 이용하기 때문에 사이드 이펙트에 대한 고려도 덜 수 있죠.

단점은 결국 레거시 로직은 그대로 남아있다는 점과, 커스텀 이벤트들을 관리해야 한다는 점이 있겠네요. 

2. 리덕스 이벤트로 소통하기

구름IDE는 상태관리 툴로 리덕스(redux)를 사용합니다. 하지만, 레거시 코드에서 관리 중인 상태가 자동으로 리덕스 스토어(redux store)에 반영되지는 않습니다. 레거시 코드에서 리덕스 이벤트를 디스패치(dispatch)하면, 리액트 코드에서도 같은 상태를 유지할 수 있죠.

이렇게 리덕스 이벤트를 사용하면서 한 가지 생각이 떠올랐습니다. ‘모달의 상태를 리덕스가 관리하면, 새로운 모달을 만들 때 리액트로만 개발할 수 있지 않을까?’ 예를 들어서, 어떤 메뉴 아이템을 눌러 모달을 띄워야 한다고 가정해 봅시다. 그런데 그 부분이 모두 제이쿼리로 작성되어 있다면요?

이전에는 제이쿼리 코드를 추가하거나 수정하는 식으로 모달을 만들었습니다. 하지만 리덕스로 모달의 상태를 관리하고 있는 지금은 제이쿼리 코드에서 다음과 같이 리덕스 이벤트를 디스패치하면 모달이 나타납니다.

reduxStore.dispatch({
		type: 'modal/open',
		payload: {
			type: 'DeleteConfirmModal',
			props: {
				filePaths,
			},
		},
	});

리액트에서는 다음과 같이 처리합니다.

// ModalManager.jsx
// ...
function ModalManager({ openedModal }) {
	if (!openedModal) return null;
	const modalType = modalIndex[openedModal.type];
	const modalComponent = modalType.component;
	const modalFunction = modalType.props;

	return (
		<ModalBoundary>
			<ModalComponent {...modalFunction} />
		</ModalBoundary>
	);
}

const mapStateToProps = (state) => ({
	openedModal: state.modal.opened,
})

// useModal.jsx
// ...
function useModal(type) {
	const dispatch = useDispatch();

	const opened = useSelector(state => state.modal.opened);
	const isOpen = opened?.type === type;
	
	const openModal = payload => dispatch(open(payload));
	const closeModal = payload => dispatch(close(payload));

	return { isOpen, openModal, closeModal };
}

modalIndex라는 파일에서 모든 모달을 익스포트(export)합니다. 리덕스에서 관리 중인 opened 상태에 모달명이 추가되면, ModalManager가 해당 모달을 렌더링하는 식입니다. 리액트에서 모달을 open하고자 한다면, useModal 훅으로 openModal(‘모달이름’)처럼 선언적으로 모달을 열 수 있죠.

마치며

모달 동작 방식을 개편 뒤 언젠가는 이 아이디어를 포스트로 공유하면 좋겠다고 생각했습니다. 레거시와 공존하도록 코드를 작성했던 당시(22년 2월), 저는 2개월차 신입이었기 때문에 이 방식이 ‘좋은 방법’인지 확신할 수 없었습니다. 당시에는 모달을 선언적으로 관리하는 참고할 만한 자료를 찾기 힘들었거든요. 그래서 오랜 시간 고민하고 또 고민했었습니다.

이제는 이와 유사한 방식으로 모달을 관리하는 사례를 어렵지 않게 찾을 수 있습니다. 과거에 이 방식을 고안하고 고심했던 나에게 치얼스🍺

2년 전에 개편했던 모달 관리 방식은 여지껏 별다른 문제 없이 잘 작동하고 있습니다. 사용하기 편하다는 동료들의 피드백을 받을 때마다 뿌듯함을 느낍니다. 

이 방식을 고안하며 내가 작성한 코드가 또 다른 레거시가 되지 않기를 정말 바랐거든요. 하지만 제가 짰던 코드들도 결국, 누군가에게는 레거시가 될 수밖에 없겠죠. 그때 저의 레거시를 다른 누군가가 또 다시 슬기롭게 풀어내길 바라면서 레거시와 소통했던 이야기를 마칩니다.

Posted by
wynter.kim

구름 GDS TOOLS 스쿼드의 리더를 맡고 있습니다. 든든한 개발자가 되기 위해 노력하고 있습니다.