안녕하세요, 이제 개발자로 일한 지 3년이 다 되어 가는 프론트엔드 주니어 개발자입니다.
대부분의 개발자들은 프레임워크를 이용해 개발하는데요, 이런 프레임워크는 잘 사용하면 개발자와 개발 커뮤니티에 많은 것들을 가져다 주지만, 잘못 사용하면 코드를 프레임워크의 노예로 만들어버릴 수 있는 강력한 힘을 가지고 있습니다.
이 글을 읽는 분들이라면 아마도 프레임워크를 이용해 보셨을 것이고, 프레임워크와 맞지 않는 기능을 구현하기 위해 씨름해보셨을 것이고, 프레임워크가 가이드하는 코드와 내가 구현하고 싶은 코드의 스타일이 달라서 고통받아보셨을 것이고, 프레임워크의 버전을 올리며 코드를 마이그레이션하느라 괴로워해보셨을 것입니다.
프레임워크는 나를 위해 발전하지 않기 때문에 언젠가 프레임워크와 사이가 틀어질 수도 있습니다. 이렇게 프레임워크가 나를 배려하지 않았을 때 조금이라도 고통을 덜 겪으려면, 가능한 프레임워크가 코드의 핵심부에 침투하지 못하도록 바깥쪽에 위치시키고, 프레임워크 커뮤니티와 꾸준히 대화하는 게 중요합니다.
이번 글에서는 이 내용을 좀더 자세하게 다뤄보려 합니다.
💡 JavaScript 와 웹 생태계에서의 경험을 바탕으로 작성한 글이기에 다른 생태계와는 맞지 않는 내용이 있을 수 있으며, 글에 나오는 많은 예시들도 이 생태계의 것들로 이루어져 있습니다.
프레임워크(framework) 에 대한 정의는 여러 설명이 있지만, 대체로 개발자가 소프트웨어를 만드는 데에 도움을 주는, 뼈대 역할을 하는 것들을 말합니다. 프론트엔드에서는 Next.js
, SvelteKit
등이 있고, 백엔드에서는 Spring Boot
나 Django Rest Framework
등이 있습니다. 라이브러리 / 프레임워크 / 메타 프레임워크
사이의 경계는 조금 애매하곤 하기 때문에, 넓게 보면 React
나 @tanstack/react-query
같은 것들도 프레임워크라고 볼 수 있겠습니다.
💡 이 글에서는 사용자의 코드 구조에 영향을 끼치는 라이브러리들까지 프레임워크라고 부르겠습니다. 가령
redux
등도 포함됩니다.
프레임워크를 사용하면 많은 이점이 있고 대부분의 입문자들이 프레임워크를 이용해서 개발을 배우기 시작하기 때문에 최근 많은 서비스 개발자들은 프레임워크 등 다른 사람들이 만들어둔 도구 위에서 코드를 작성합니다.
이렇게 프레임워크를 이용해서 개발을 하다 보면 어느새 마치 종교처럼 프레임워크의 신봉자가 되곤 하는데요, 프레임워크의 매뉴얼에 맞춰 개발하며 도구의 효율을 100% 뽑아내는 건 좋은 현상이지만, 자칫하면 프레임워크에 코드가 잠식당하여 원하는 기능을 개발하기 어려워지거나 라이브러리들간의 버전을 맞추느라 끙끙대게 될 수도 있습니다.
먼저 프레임워크가 가져다 주는 좋은 점들에 대해 간단하게 알아보겠습니다.
보통 프레임워크는 개발자가 "깊게 고민할 필요 없이 가져다 쓰기만 하면 되도록" 많은 기능들을 제공합니다. 대표적으로 Next.js
와 같은 프레임워크는 yarn start
만 하면 메세지를 수신하여 HTML을 반환하는 서버를 띄웁니다. 이 과정을 직접 개발하려면 많은 코드를 작성해야 했겠지만, 프레임워크가 지원해주는 덕분에 개발자는 이런 부분에 대해 고민할 필요가 없습니다.
또한 많은 프레임워크는 개발자가 작성한 코드를 자기가 원하는 타이밍에 적절히 수행해주는데요, 이를 통해 개발자들은 어떤 동작을 할지 선언하는 것 에만 집중하면 되고 그게 실제로 언제 어떻게 수행되는지 등은 프레임워크가 알아서 해줄 거라고 믿고 신경쓰지 않아도 됩니다.
동일한 프레임워크를 이용한다면, 개발자들끼리 소통하는 시간을 크게 줄일 수 있습니다. 저도 react-query
를 이용해 봤고 상대도 react-query
를 이용해 봤다면, 저는 상대 개발자에게 제가 넣어준 staleTime
변수가 무슨 일을 하는지 따로 설명하느라 시간을 쓸 필요가 없습니다.
이런 점은 코드레벨을 넘어 개발자 채용과 육성에도 도움을 주기에 회사에도 직접적으로 영향을 끼치는 장점입니다.
이렇게 다양한 사람들이 같이 이용하는 프레임워크는 보통 사용자들끼리 커뮤니티를 형성해서 소통하곤 하는데요, 이 역시 개발 시간을 단축해줍니다.
Stack Overflow에 "내가 개발한 1000줄짜리 코드가 어떤 동작을 하길 기대했는데 실제로는 다르게 동작해. 이유가 뭘까?" 라며 1000줄짜리 코드를 파일로 올려두면 아무도 대답해주지 않고 현실적으로 불가능하겠지만, "next/link
doesn't work as expected" 라고 GitHub에 이슈를 올리면 메인테이너들과 개발자들이 성심성의껏 답변해줍니다.
또한 선배 개발자들이 같은 프레임워크를 이용하며 먼저 고생하고 올려둔 블로그 글들도 개발자에겐 큰 힘이 됩니다.
반면 프레임워크를 이용했을 때에 생기는 단점들도 있습니다.
프레임워크 역시 사람이 만든 코드인 만큼 버그가 있을 수 있습니다. 물론 이건 제가 만든 코드도 마찬가지이지만, 다른 사람이 만든 코드는 제가 고칠 수 없다는 점이 문제가 됩니다. 오픈소스라면 직접 contribute할 수 있지만 이는 언제까지나 PR을 올리는 것 정도까지만 가능한 것이고, 반영 후 배포되어 제 프로젝트가 정상 동작하게 만드는 데까지는 시간이 많이 걸립니다. 비교적 커뮤니티가 큰 프레임워크는 배포 전에 버그 및 호환성을 충분히 검증하기에 이런 일이 일어나지 않지만, 커뮤니티가 작은 프레임워크에서는 "프레임워크 자체 버그 때문에 ~~~ "라는 말을 종종 하게 됩니다. 이는 안정성이 중요한 프로젝트일수록 크게 다가오는 문제입니다.
많은 무료 프레임워크들은 오픈소스로 관리되는데요, 이를 관리하는 메인테이너들이 개발자들에게 돈을 받고 하는 일이 아니다 보니 프로젝트가 버려져도 개발자들이 할 수 있는 게 딱히 없습니다. 실제로 유지보수가 되지 않고 버려지는 라이브러리들이 종종 있고, 세계적인 대기업인 meta에서 개발한 recoil 마저도 거의 버려지다시피 하여 이슈가 쌓이고만 있는 모습을 볼 수 있습니다.
이슈가 쌓이면서 개발자들의 걱정이 커지고 있는 recoil개발자는 프레임워크가 변경되는 방향에 의견을 낼 수는 있지만, 그 이상의 권한은 없습니다. 많은 프레임워크들은 최대한 개발자의 요구사항에 맞춰 발전하려고 노력하겠지만 그러지 않을 수도 있습니다. 가령 tanstack query는 (합당한 이유가 있었지만) useQuery
훅에서 널리 이용되던 onSuccess
와 onError
등의 콜백을 제거해 버렸고, turbo 라는 프레임워크 메인테이너는 마음대로 현재 JS 생태계에서 정석이라고 할 수 있는 typescript 지원을 중단해 버렸습니다.
개발자들은 이런 변화가 생길 때마다 눈을 질끈 감고 버전을 올리고 코드를 마이그레이션하느라 고통받아야 합니다.
가장 중요한 부분입니다. 프레임워크는 끊임없이 등장하고 변화합니다. 현재 사용하고 있는 프레임워크보다 더 좋은 프레임워크가 나올 수도 있고, 실제로 지금까지 웹의 역사상 더 좋은 프레임워크는 계속 등장해 왔습니다. PHP, .NET, AngularJS, ReactJS (SPA), NextJS 에 이르기까지 최신 트렌드는 계속 변경되어 왔고, 사실 지금도 가장 널리 쓰이는 next.js보다 더 "좋은" 프레임워크는 존재합니다. 우리가 Astro나 SvelteKit 등의 프레임워크로 마이그레이션하지 못하는 데에는 커뮤니티 사이즈라는 이유도 있지만, 기존 프로젝트를 마이그레이션하는 비용이 너무 크다는 점도 있을 것입니다.
사실 SvelteKit까지 갈 것도 없이, 이미 많은 프론트엔드 개발자들은 Next.js pages router 에서 app rotuer 로 마이그레이션하는 데에 난항을 겪고 있습니다. 특히 Next.js 가 소스코드의 핵심 로직에 더 깊게 침투해있는 프로젝트일수록 더 그렇습니다.
많은 경우, 프레임워크 제작자들은 개발자가 프레임워크를 떠나는 것을 좋아하지 않습니다. 이를 위해 프레임워크가 개발자의 소스코드에서 더욱더 중요한 역할을 하길 원하고, 개발자가 프레임워크를 떠나기 어려워하길 원합니다. 이를 위해 편의기능처럼 보이는 다양한 유혹들을 제공하고 때론 강제합니다.
💡 프레임워크 제작자가 이를 의도했는지 아닌지는 사실 중요하지 않습니다. 중요한 건, 실제로 많은 프레임워크들은 이런 유혹을 제공하고 강제한다는 것입니다.
예를 들면, next.js 의 파일 시스템 기반 라우팅 도 이런 역할을 합니다. next.js 는 파일 디렉토리 구조를 따라 페이지를 보여줍니다. "어느 경로에 어느 페이지가 보여야 한다"는 어플리케이션의 핵심 정책을 구현하기 위해서는 next.js 가 시키는 대로 파일을 위치시켜야 하고, next.js의 라우팅 방식과 개발자가 구현하고자 하는 핵심 정책 사이에 추상화의 벽을 세울 수 없습니다. 이런 구조의 어플리케이션은 다른 프레임워크로 마이그레이션하기 어려운데, 가령 라우팅 구조를 파일 시스템 기반 대신 src/routes.ts
에 정의하도록 강제하는 프레임워크가 등장한다면 기존 파일 구조는 굉장히 괴상해 보일 것이기 때문입니다.
이렇게, 프레임워크는 장점도 있지만 단점도 있습니다. 프레임워크의 장점은 현실적으로 포기하기 힘들 만큼 강력하지만, 반대로 단점은 어플리케이션을 순식간에 레거시로 만들어버릴 수 있을 만큼 치명적입니다. 프레임워크를 잘 이용하기 위해서는 영리하게 프레임워크의 장점만 취하고 단점을 마주하지 않도록 노력해야 합니다.
모든 어플리케이션은 존재 목적이 있습니다. 가령 유명한 dan abramov 의 개인 블로그 https://overreacted.io 는 사용자들에게 댄의 아티클을 보여주기 위한 목적으로 존재합니다. 절대 그 사이트가 Gatsby.js로 되어 있는 게 목적이 아닙니다. Gatsby.js는 어플리케이션을 구성하기 위한 하나의 수단일 뿐입니다. 다른 어플리케이션들도 마찬가지입니다. 어느 어플리케이션도 "내가 이런 멋진 프레임워크로 되어 있어!" 라는 걸 존재 목적으로 하지 않습니다. 즉, 이 목적과 기능이 프레임워크보다 훨씬 중요하고 핵심적입니다.
비유하자면, 어플리케이션이 직장인이라고 한다면 프레임워크는 지하철 정도의 역할입니다. 우리 직장인들은 지하철이 파업한다고 해도 버스, 택시, 자전거, 재택근무 등 다양한 다른 수단을 이용해서 출근할 수 있고 이 점은 업무에 당장은 영향을 줄지언정 업무 내용 자체에 치명적인 영향을 주지 않습니다. 프레임워크는 딱 이 정도 역할이어야 합니다.
프레임워크를 영리하게 이용하되, 프레임워크를 너무 믿어서 소스코드를 맡겨버리는 실수를 저지르지는 않아야 합니다.
가령 리액트로 개발하다 보면, 분명 "모달의 열림 닫힘 상태"는 useState
(또는 그에 준하는 상태관리 방식) 로 관리되어야 합니다. 이 로직이 리액트와 결합되지 않을 수는 없습니다.
하지만 api 콜 엔드포인트나 폼의 유효성 검사 로직은 리액트가 아닌 곳에서도 이용할 수 있어야 합니다. 개인적으로 좋아하는 질문은 "스벨트여도 이 코드 100% 똑같이 쓸 수 있나?" 입니다.
가령 아래와 같은 코드는 리액트에 종속적인 "컴포넌트" 라는 단위 내에 데이터 포맷팅 로직과 api 엔드포인트에 대한 세부사항이 모두 들어 있습니다. 이 코드를 스벨트로 마이그레이션한다면, 파일 전체를 건드려야 합니다.
// TodoPage.tsx
const TodoPage = ({ page }: { page: number }) => {
const [data, setData] = useState();
useEffect(() => {
let cancelled = false;
fetch(`https://jsonplaceholder.typicode.com/todos/${page}`)
.then(response => response.json())
.then((r) => !cancelled && setData({ id: r.id, content: r.title }));
return () => {
cancelled = true;
};
}, [page]);
return <ul>{data.map((d) => <TodoItem key={d.id} todo={d} />}</ul>;
}
대신 파일을 분리하여 이렇게 작성했다면, 마이그레이션할 때 적어도 fetchTodo.ts
파일은 안 건드려도 되니 더 쉬웠을 것입니다.
// fetchTodo.ts
// 이 로직은 프레임워크가 리액트든 스벨트든 앵귤러든 상관없이 쓸 수 있다
export const fetchTodo =
(page: number) =>
fetch(`https://jsonplaceholder.typicode.com/todos/${page}`)
.then(response => response.json())
.then((r) => ({ id: r.id, content: r.title }));
// TodoPage.tsx
import { fetchTodo } from '@/apis/fetchTodo';
const TodoPage = ({ page }: { page: number }) => {
const [data, setData] = useState();
useEffect(() => {
let cancelled = false;
fetchTodo(page).then((res) => !cancelled && setData(res));
return () => {
cancelled = true;
};
}, [page]);
return <ul>{data.map((d) => <TodoItem key={d.id} todo={d} />}</ul>;
}
간혹 비즈니스 로직을 커스텀 훅에 작성하는 컨벤션들을 보곤 하는데, 틀렸다고 말할 수는 없지만 리액트에 강하게 결합되는 코드라고 생각합니다. 프레임워크와 분리하는 게 부담스럽지 않다면, 프레임워크와 무관한 코드는 프레임워크와 분리되는 게 좋습니다.
💡 좀더 나가면 아키텍처 관점에서
fetch
라는 api도, 우리가 쓰는 웹이나 HTTP와 같은 기술조차도 사실은 세부사항일 뿐이기에 모두 의존성 주입으로 처리해주면 정말 유연한 소프트웨어가 될 수 있습니다. 이 내용은 이 글에서 다루기엔 너무 방대해지므로 다음 기회에 다뤄 보겠습니다.
그럼에도 불구하고 많은 경우 프레임워크에 의존하지 않는 코드를 짜기란 어려운 일입니다. 불필요하게 코드량이 많아져서 꺼려질 수도 있고, 애초에 불가능할 수도 있습니다.
이럴 때 프레임워크의 변화를 조금이라도 더 빨리 알아차릴 수 있도록 제작자가 커뮤니티를 통해 이야기해주는 것들을 주기적으로 확인하면 도움이 됩니다. 가령 @tanstack/react-query
라이브러리는 2023년 10월 17일에 v5를 릴리즈하며 useQuery
함수의 파라미터 오버로딩을 제거하고 객체 타입만 허용했는데요, 이 소식은 사실 1년 전인 2022년 10월에 이미 커뮤니티를 통해 알려졌습니다. 좋은 프레임워크 메인테이너들은 이렇게 변경될 것들에 대해 미리 공지해서 준비할 시간을 충분히 주곤 합니다.
따라서 이 소식을 미리 알고 있던 개발자들은 미래에 마이그레이션하느라 고통받지 않도록 더이상 없어질 방식으로 코드를 작성하지 않았을 것이고, 그 결과 v4 에서 v5 로 마이그레이션할 때에 더 적은 노력을 들일 수 있었을 것입니다.
이런 소식을 더 빨리 접하는 데에 커뮤니티나 개발 뉴스 등을 챙겨보는 것이 도움이 됩니다.
프레임워크는 어플리케이션을 구성하는 세부사항입니다. 프레임워크는 개발자들에게 많은 도움을 주는 고마운 존재이지만, 잘못 이용한다면 코드가 프레임워크의 늪에 빠져 벗어나지 못하게 될 수도 있습니다. 개발자들은 항상 이 점을 인지하고 신경쓰면서 프레임워크가 소스코드에 너무 깊게 침투하지 못하도록 선을 그어야 합니다. 선을 긋는 게 어렵다면, 제작자가 어떤 생각을 하고 있으며 어떤 형태로 발전/변화하게 될지 미리미리 알 수 있도록 커뮤니티를 종종 확인해 두는 게 좋습니다.
로버트 C. 마틴의 말을 인용하며 글을 마칩니다.
프레임워크와의 첫만남부터 바로 결혼하려 들지 말라. 결혼 서약에 앞서 잠시 동안 연애를 할 수 있는 방법이 있는지 확인하라. 가급적이면 프레임워크를 가능한 한 오랫동안 아키텍처 경계 너머에 두자. 아마 젖소를 사지 않고도 우유를 얻는 방법을 찾을 수 있을 것이다.
로버트 C. 마틴, 클린 아키텍처 32장: 프레임워크는 세부사항이다
너무 멋진 글이네요! 개발자로써 좋은 관점이 될 글이라고 생각합니다. 감사합니다 :) 잘 읽고 갑니다