이번 컨퍼런스에서 기존에 사용하던 Vue.js와 Quasor 대신에 새롭게 Next.js를 사용하여 프로젝트를 진행한 경험에 대해서 발표를 하게되었습니다. 새로운 기술인 Next.js의 대표적인 장점과 도입하는 과정 및 문제에 대한 내용이였습니다. 앞으로 Next.js를 새롭게 도입하려는 계획이 있거나 관심이 있으신 분들에게 도움이 되었으면 하고자 컨퍼런스에서 발표했던 내용들을 포스트를 통하여 다시 한번 정리하고자합니다.

📖 Next.js의 대표적인 장점

커뮤니티의 크기

Vue.js는 낮은 러닝커브와 가벼운 크기, 반응형 데이터 바인딩 등 다양한 장점을 가지고 있는 언어입니다. 다만, 상대적으로 프론트엔드 개발의 주류를 차지하고있는 React와 비교했을때에는 유저수가 작습니다. user_usage.png

사용자의 규모는 커뮤니티의 크기에 직접적으로 영향을 미치는 요인이기도하고 문서화의 완성도나 일부 라이브러리나 서드파티 도구는 React에 비해 부족할 수 있습니다. Next.js는 React의 프레임워크이기 때문에 프레임워크로서의 다양하고 강력한 내장 기능들은 물론, React의 커뮤니티를 공유하는 장점 역시 가지고 있다고 할 수 있습니다.

파일 기반 라우팅

Next.js가 제공하는 다양한 내장 기능들중 가장 대표적인 것이 바로 파일 기반 라우팅입니다.

파일 기반 라우팅은 복잡한 라우팅 설정 없이 폴더 구조만으로 라우트를 정의할 수 있는 기능입니다. vue_route.png

Vue.js의 경우에 페이지를 새롭게 생성하거나 수정해야할 경우에 라우팅 설정 파일을 수정하거나 생성해야합니다. 하지만 Next.js의 파일 기반 라우팅은 이러한 과정을 생략하고 폴더와 파일 구조만으로 라우팅 설정을 할 수 있는 기능을 제공하고 있습니다.

app_routing.png

위의 사진과 같이 app/ 경로에 page.tsx를 만들게되면 **/**경로에 페이지가 매핑되게됩니다. 이와 같이 폴더를 만들고 그안에 page.tsx를 만들면 page.tsx가 자동으로 해당 폴더의 구조와 동일한 경로로 매핑이 됩니다.
뿐만아니라 중첩 라우팅, 동적 라우팅 등의 다양한 라우팅 기능을 파일 구조만으로 직관적이고 편리하게 라우트를 설계할 수 있습니다.

이외에도 **SSR(서버 사이드 렌더링)**과 **CSR(클라이언트 사이드 렌더링)**을 컴포넌트마다 간단하게 원하는 대로 설정을 하여 사용할 수 있고, 빌드시에 미리 HTML을 생성해 두었다가 클라이언트에서 요청이 들어올때 생성해둔 HTML을 보내주는 SSG(정적 사이트 생성), 백엔드 API를 같은 프로젝트 내에서 관리할 수 있는 API 라우트 기능과 API 요청이나 페이지 요청을 처리하는 미들웨어 등의 내장 기능들을 지원합니다.


🧎🏻‍♂️ 도입

디자인 시스템

디자인 시스템은 UI를 설계하고 구축하기 위한 일련의 방식과 규칙을 의미합니다. 쉽게 말해, 일관된 사용자 경험을 제공하기 위해 팀이 함께 사용하는 디자인 규칙이라고 할 수 있습니다.

UI 컴포넌트, 컬러 팔레트 시스템 등으로 구성되며 특히 대규모 프로젝트에서는 디자인과 개발의 생산성을 크게 높여주는 중요한 도구로 자리 잡고 있습니다.

디자인 시스템이 필요한 이유가 무엇일까요? 바로 코드 통일성과 중복 제거에 유리하기 때문입니다.
프로젝트를 진행하다 보면, 공통 페이지모달 레이아웃처럼 동일한 레이아웃을 가진 경우가 많이 있는데요, 하지만 디자인 시스템 없이 여러 개발자가 작업하게 되면, 개발자마다 자신의 기준에 따라 컴포넌트를 설계하게 됩니다.
이렇게 만들어진 컴포넌트는 마진, 폰트 크기 등 세부적인 요소들이 조금씩 다를 수 있고 코드 또한 중복되어 재사용성이 떨어지거나 확장성이 부족한 경우가 생길 수 있습니다. 동일한 레이아웃임에도 불구하고 효율성이 떨어지는 결과를 불러올 수 있습니다.

즉, 디자인 시스템은 통일된 기준을 제공하기 때문에 효율적인 협업과 컴포넌트의 설계와 구현 단계를 효과적으로 진행할 수 있도록 도와줍니다.

이러한 디자인 시스템도 여러가지 방식이 있지만 저희가 도입한 방법은 아토믹 디자인입니다.

아토믹 디자인

atomic_design.png 아토믹 디자인은 화학적 원리에서 영감을 받은 디자인 시스템 방법론입니다.

모든 것이 원자로 이루어지듯이, 컴포넌트도 원자와 같은 접근방식으로 가장 작은 단위부터 점진적으로 결합하여 더 큰 구조를 이루는 방식입니다.

단위설명
원자(Atoms)더 이상 분해할 수 없는 기본 컴포넌트 단위
분자(Molecule)Atom이 결합하여 하나의 작업을 수행하는 단위
유기체(Organism)Atoms와 Molecules로 구성된 상위 단위
템플릿(Template)레이아웃과 구조를 정의하는 와이어프레임 단위
페이지(Page)실제 컨텐츠, 템플릿의 인스턴스

아토믹 디자인의 경우에도 단점이 존재하게 됩니다. 아토믹 디자인은 상세하게 구조화해야 한다는 점에서 자유도가 떨어질 수 있습니다. 특히, 작은 단위에서부터 모든 것을 구조화해야 하다 보니, 초기 설계에 많은 시간이 필요합니다. 이는 프로젝트 초기에 복잡성과 설계 비용을 증가시킬 수 있다는 단점으로 작용할 수 있습니다.

이러한 단점을 극복하기 위해, 저희는 아토믹 디자인의 개념을 단순화하여 설계하게 되었습니다.

단위설명
원자(Atoms)원자 단위와 분자 단위의 통합 단위
분자(Molecule)유기체와 템플릿의 통합 단위
유기체(Organism)실제 컨텐츠, 템플릿의 인스턴스

상태관리

상태관리는 사용자 인터페이스에서 사용자의 액션(클릭이나 이동 등)을 감지하고, 상태를 관리해주는 함수들에 따라서 상태가 업데이트되게 됩니다.

상태가 업데이트되게 되면 변경될 때 사용자 인터페이스도 같이 변화합니다. 여기서 상태는 사용자의 로그인 상태, 검색한 상품 리스트, 장바구니에 담긴 상품 등을 예시로 들 수 있습니다.

이러한 방법을 통하여 조금 더 반응적인 웹페이지를 구현할 수 있습니다.

데이터의 흐름

data_flow.png

Next.js는 리액트와 동일하게 statecontext등을 통하여 상태관리를 할 수 있습니다.

데이터는 위와 같이 단방향으로만 일어나는 특징을 가지고 있어 데이터의 흐름이 단순하고 예측 가능하며,

컴포넌트간의 관계를 명확히 정의하기 때문에 유지 보수하기에 용이하다는 장점을 가지고 있습니다.

state_context.png

다만 사진속 예시와 같이 컴포넌트의 depth가 많아질 수록 상태값을 사용하지않는 컴포넌트들에게도 props를 전달을 해줘야하기에

stateProp Drilling이나 context의 경우에는 해당 상태를 구독하는 모든 컴포넌트들의 리렌더링과 복잡한 구조가 단점이 되기도 합니다.

상태관리 라이브러리

이렇듯 최근 상태가 점점 다양해지고 복잡해지면서 상태관리 라이브러리들의 중요성과 필요성에 커지게 되었고 다양한 라이브러리들이 생겨났습니다.

state_library.png

각각의 라이브러리마다 장단점이 존재하기에 현재 도입하게된 프로젝트의 성격에 맞추어 고르고자 하였고, 꾸준한 인기를 유지하고있는 Zustand를 선택하게 되었습니다.

Zustand는 가볍고 간결한 코드를 가지고있어 러닝커브가 낮습니다.
성능 또한 이미 사용자 수가 어느정도 증명하고 있고, 상태를 전역적으로 관리해주며 구독방식을 통하여 선택적인 컴포넌트들만 리렌더링하는 장점도 가지고 있습니다.

그외에도 미들웨어를 통한 상태 변경에 대한 로깅, 캐싱, 지속화, 비동기 처리등도 지원하고 있습니다.


💡 문제 해결

스타일 적용 - Tailwind

리액트에서 스타일링을 다양한 방법중에서 빠르고 효율적으로 UI를 설계할 수 있도록 도와주는 Tailwind라는 유틸리티 기반 CSS 프레임워크를 선택하였습니다.

Tailwind는 사전 정의된 클래스들을 조합해서 스타일링을 할 수 있습니다.

다만 Tailwind의 사전 정의 클래스는 rem을 기본 단위로 사용하고 있습니다. 그렇기에 px단위로 디자인된 레이아웃을 작업할때에 []을 사용하여 직접적으로 px를 지정해주거나 rem단위로 환산하여 tailwind에서 찾아 사용해야 했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const pxToRem = (px: number, base = 12) => `${px / base}rem`;
const range = (start: number, end: number): number[] => {
    return Array.from({ length: end - start + 1 }, (_, index) => start + index);
};
const sizes = {
	...range(1, 2000).reduce<Record<string, string>>((acc, px) => {
		acc[`${px}pxr`] = pxToRem(px);
		return acc;
	}, {}),
};

그렇기에 tailwind.confing.js파일안에 변환하는 로직을 새롭게 만들어 간편한 방법으로 px를 사용할 수 있도록 하였습니다.

버전 호환성

처음 프로젝트 세팅 시, Next.js 최신버전인 15와 기본 설정인 react 19 rc로 프로젝트를 진행하려 했습니다.

하지만 Style, test, storybook등의 외부라이브러리가 react의 최신버전을 아직 따라오지 못하는 호환성 이슈가 있었습니다.

매번 —force, —legacy-peer-deps 로만 라이브러리를 설치를 해야지만 사용이 가능 했습니다.

Next.js 15 + react 18로도 사용은 할 수 있었지만, Next.js 15는 react 19를 베이스로 동작하기 때문에 react버전을 변경하는건 여러가지 호환성 이슈가 발생할 수 있다고 판단하였습니다.

현재는 정식버진이 출시가 되었지만 프로젝트 세팅 당시에 react19는 정식버전이 아닌 **RC(Release Candidate)**버전이였으므로 안정화되어있지 않아서 긴급 업데이트나 변경사항이 발생 할 수 있기 때문에 next.js 14 + react18 로 결정하였습니다.