Next.js 개인프로젝트를 진행한 과정과 느낀점을 남겨보고자 한다.
남들이 안한 특별한 주제를 하는것도 좋지만, 이번 프로젝트에서는 흔한 주제라도 Next.js를 익히고 기술적 숙련도를 올리는 목적이 더 컸다.
그렇기에, 메타의 Threads 서비스를 클론코딩 하되, 기존 서비스를 내 입맛대로 커스텀하여 프로젝트를 진행해 해보기로 결정했다.
이번 프로젝트의 가장 큰 장점은 디자인과 기획 단계에 많은 시간을 할애할 필요가 없다는 점이다. 이미 검증된 서비스인 Threads의 디자인과 사용자 경험 흐름을 그대로 차용하면 되기 때문이다.
이렇게 디자인과 기획 부분에 대한 부담이 줄어들면서, 컴포넌트 구조 설계, 데이터베이스 테이블 구성, 활용할 라이브러리 선정 등 핵심 기술 stack에 대해 더 깊이 있게 연구하고 고민할 수 있었다. 실제 개발 과정에서 가장 중요한 부분에 집중할 수 있었던 것이다.
하지만 단점은 스스로 기획한 것이 아니기에, 창의성과 독창성이 부족할 수 있다는 점이다. 비록 기술적인 측면에서는 새로운 방식을 적용할 수 있지만, 전반적인 서비스 구조와 컨셉은 기존 서비스를 모방할 수밖에 없다.
또한, 기존 서비스의 한계나 문제점을 그대로 계승할 가능성이 있다. 그리고 새로운 아이디어나 혁신적인 기능을 더하기 어려울 수 있다.
내가 선택한 주요 Next.js 13버전, tailwindCSS, react-query, supabase 등이 있다.
그때 당시 내가 왜 이 기술스택을 골랐는지에 대한 이유를 적어보려 한다.
Next.js의 경우 현재 14버전까지 출시되어 있지만, 본 프로젝트에서는 13버전을 채택하기로 했다. 14버전이 아직 출시된 지 오래되지 않았기 때문에, 버전의 안정성 측면에서 13버전이 더 검증되어 있다고 판단했기 때문이다.
새로운 버전은 업데이트된 기능과 향상된 성능을 제공하지만, 동시에 버그나 호환성 이슈 등의 잠재적인 위험도 존재한다. 특히 기업 환경에서는 안정성이 매우 중요한 요소이기 때문에, 충분한 테스트를 거쳐 안정성이 입증된 버전을 선호하는 경향이 있을거라 생각했다.
따라서 이번 프로젝트에서도 Next.js 13버전을 선택함으로써, 검증된 안정성과 신뢰성을 확보하고자 했다. 물론 새로운 버전의 기능도 지속적으로 모니터링하여 적절한 시기에 업그레이드할 예정이다.
이전 프로젝트들 에서는 tailwindCSS가 아닌 styled-componenets를 주로 사용했었다. 하지만 이번 프로젝트 Next.js 13버전부터 나온 서버컴포넌트를 활용하려면 styled-componenets보단 tailwindCSS가 더 적합하다고 판단했다.
Next.js에서는 CSS-in-JS 방식의 스타일링도 가능하지만, 이를 위해서는 'use client'를 선언하여 클라이언트 컴포넌트로 지정해야 한다. 이벤트 리스너, 브라우저 API, React Hooks 등이 필요 없는 정적인 페이지에서조차 CSS-in-JS 라이브러리를 사용하기 위해 매번 Hydration 과정을 거치게 되면, 서버 컴포넌트의 활용도가 떨어지게 된다.
서버 컴포넌트는 Next.js의 주요 기능 중 하나로, 초기 렌더링을 서버에서 처리하여 더 나은 초기 로드 성능과 SEO 최적화를 제공한다. 하지만 CSS-in-JS 라이브러리를 사용하려면 클라이언트 컴포넌트로 전환해야 하므로, 서버 컴포넌트의 이점을 활용하기 어려워진다.
따라서 본 프로젝트에서는 Tailwind CSS를 채택하기로 했다. Tailwind CSS는 유틸리티 기반의 CSS 프레임워크로, 별도의 JavaScript 런타임이 필요하지 않다.
이를 통해 서버 컴포넌트의 활용도를 높이고, 초기 로드 성능과 SEO 최적화의 이점을 누릴 수 있다. 또한 TailwindCSS는 러닝커브가 있는 편이지만 잘 익히기만 한다면, 개발 생산성을 높이고 일관된 디자인 시스템을 구축하는 데 도움이 될거라 판단했다.
이번 프로젝트에서는 이전에 사용했던 Firebase를 대신하여 다른 솔루션을 채택하기로 했다. 이는 크게 두 가지 이유 때문이다.
첫째, Firebase는 NoSQL 기반의 데이터베이스를 제공하는데, 이는 관계형 데이터베이스와 달리 데이터 간의 Join 연산이 불가능하고 데이터 처리 완전성이 보장되지 않는다. SNS 서비스와 같이 다양한 데이터 엔티티와 관계가 존재하는 이번 프로젝트에서는 이러한 NoSQL의 한계가 적절하지 않다고 판단했다.
둘째, Firebase를 활용한 검색 기능의 정확도가 만족스럽지 않았다. Algolia나 Elastic Search와 같은 전문 검색 솔루션을 활용하면 검색 정확도를 높일 수 있겠지만, 이번 프로젝트에서는 그렇게 하기에는 과도한 노력이 필요할 것 같았다. 검색 기능 자체가 중요한 핵심 기능은 아니었기 때문이다.
따라서 이번 프로젝트에서는 관계형 데이터베이스를 가진 Supabase를 채택하기로 했다. 이를 통해 데이터 처리의 완전성과 일관성을 보장하고, 개발 리소스를 핵심 기능에 더 집중할 수 있을 것으로 기대했다.
이번 프로젝트에서 Supabase와 React-Query를 함께 사용하기로 결정했다. 두 라이브러리를 결합함으로써 서버 상태 관리 최소화, 캐싱 및 업데이트 최적화, 개발 생산성 향상 등의 장점을 얻을 수 있었다.
React-Query는 클라이언트 상태 관리를 처리하여 전체 상태 관리를 단순화해주었다. 또한 React-Query는 데이터 캐싱, 자동 업데이트, 오류 처리 등의 기능을 제공하여 데이터 fetching 프로세스를 최적화해주었으며, 이를 통해 불필요한 네트워크 요청을 줄이고 앱 성능을 향상시킬 수 있었다.
반면, Supabase와 React-Query를 함께 사용하면서 전체 시스템이 비교적 복잡해졌고, React Query의 클라이언트 측 캐싱 사용으로 인해 대량의 데이터 처리 시 메모리 문제가 발생할 수 있으며 잘못 구성된 경우 불필요한 재렌더링이 발생할 수 있는 단점이 있었다.
이러한 단점에도 불구하고 상태 관리 단순화, 개발 생산성 향상, 성능 최적화, 커뮤니티 지원 등의 장점이 더 크다고 판단하여 React Query를 채택하기로 했다.
React를 주력으로 개발을 진행하다가 Next.js를 이용하여 개발을 진행하니 처음에는 진행이 느렸다. Next.js의 SSR과 13 버전부터 바뀐 app router와 서버컴포넌트에 대한 지식이 부족했었다. 그렇기에 먼저 공식문서를 읽어보며 공부를 하는게 우선이였다.
Next.js를 공부하며 개발하는 과정에서 생긴 트러블슈팅에 대해서 적어보려고 한다.
Supabase에서는 기본적으로 제공하는 signUp 메서드를 통해 회원가입 기능을 구현할 수 있다. 하지만 이 방식으로 회원가입을 진행해도 애플리케이션 고유의 users 테이블에는 자동으로 데이터가 삽입되진 않는다.
이번 프로젝트에서는 사용자가 이메일 또는 OAuth 인증으로 회원가입할 때, 별도로 구성한 users 테이블에도 사용자 정보가 기록되어야 했다. 이는 추후 사용자 정보 관리나 부가 기능 구현에 필수적인 요소라고 생각했다.
이러한 요구사항을 해결하기 위해 PostgreSQL의 trigger 기능을 활용했다. Supabase의 auth.users 테이블에 새로운 사용자가 추가되면, 사전에 정의한 트리거 함수가 실행되어 해당 사용자 정보를 users 테이블에 자동으로 삽입하도록 설정했다.
이렇게 함으로써 회원가입 프로세스 동안 별도의 작업 없이도 users 테이블에 사용자 데이터가 기록되며, 데이터 중복을 방지할 수 있었다. 또한 trigger 함수 내에서 필요한 추가 로직을 구현할 수 있어 유연성도 확보할 수 있었다.
-- trigger함수
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = public
as $$
begin
INSERT into public.users (uuid, email, user_name, avatar_url)
VALUES (
new.id,
new.email,
new.raw_user_meta_data->>'user_name',
COALESCE(new.raw_user_meta_data->>'avatar_url', 'https://ohpldinktpofyatjafei.supabase.co/path-name...')
);
return new;
end;
$$;
-- 유저가 생성될때마다 trigger함수 실행
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
React에서는 react-router-dom을 이용해 PrivateRoute 컴포넌트를 만들어 Private Routing을 구현했지만 App router환경의 Next.js에서는 그렇게 하지 못하기에 다른 방법이 필요했다.
공부해보니 크게 두가지 방법이 있었다. HOC를 이용한 routing 방법, 그리고 Next.js의 middleware를 이용한 방법이 있었다.
HOC(Higher-Order Component)를 이용한 방법은 인증 로직을 별도의 컴포넌트로 분리하여 필요한 페이지에 적용할 수 있다는 장점이 있다. 하지만 이 방식은 클라이언트 측에서 실행되기 때문에 초기 렌더링 시에는 인증 상태를 확인할 수 없어 UI 깜박임 현상이 발생할 수 있었다.
반면, Next.js의 middleware는 서버 측에서 실행되므로 초기 렌더링 시에도 인증 상태를 확인할 수 있다. 또한, 라우팅 로직을 한 곳에서 중앙 집중식으로 관리할 수 있어 유지보수가 용이하다고 생각했다.
단점으로는 middleware 설정이 다소 복잡할 수 있다는 점이 있었지만 크게 문제되진 않았다.
결론적으로 이번 프로젝트에서는 middleware를 선택했다. 이는 인증 상태에 따른 UI 깜박임 현상을 방지하고, 초기 렌더링 시에도 안전한 인증 처리를 보장하기 위함이다.
또한, 라우팅 로직을 중앙 집중식으로 관리할 수 있어 유지보수 측면에서도 이점이 있다. middleware 설정의 복잡성은 초기 러닝커브가 있겠지만, 장기적으로는 이러한 이점이 더 크다고 판단했다.
// middleware.ts
// 로그아웃시 접근하지 못하는 페이지
const logedOutRoute = [
END_POINT.MAIN,
END_POINT.SEARCH,
END_POINT.USER,
END_POINT.COMMENT,
END_POINT.ACTIVITY,
];
// 로그인시 접근하지 못하는 페이지
const logedInRoute = [
END_POINT.ROOT,
END_POINT.SIGN_UP,
END_POINT.RESET,
END_POINT.RECOVER,
];
export function middleware(request: NextRequest) {
const { cookies } = request;
const hasCookie = cookies.has('my-access-token');
if (!hasCookie && logedOutRoute.includes(request.nextUrl.pathname)) {
return NextResponse.redirect(
new URL(END_POINT.ROOT, request.nextUrl.origin),
);
}
if (hasCookie && logedInRoute.includes(request.nextUrl.pathname)) {
return NextResponse.rewrite(
new URL(END_POINT.MAIN, request.nextUrl.origin),
);
}
return NextResponse.next();
}
Extra attributes from the server: class,style
원인은 생각보다 찾기 쉬웠다.
서버에서 페이지를 렌더링할 때, theme는 서버 측에서 알 수 없는 상태이기 때문에 undefined로 인식된다. 그러나 MainThemeProvider가 렌더링되면 theme 상태가 존재하게 되어, 서버와 클라이언트 측의 구조가 달라지게 된다. 이로 인해 hydration 에러가 발생하는 것이다.
이 문제를 해결하기 위한 방법은 크게 두 가지가 있었다. 첫 번째는 useEffect를 사용하여 MainThemeProvider가 마운트될 때까지 기다리는 것이고, 두 번째는 최상위 HTML 태그에 suppressHydrationWarning을 명시하여 hydration 경고를 끄는 것이었다.
결론적으로 나는 두 번째 방법인 suppressHydrationWarning을 명시하는 방식을 선택했다. 첫 번째 방법은 MainThemeProvider가 마운트되고 hydration이 완료될 때까지 하위 자식 컴포넌트의 렌더링이 지연되어 SSR(Server-Side Rendering)의 이점을 활용할 수 없는 문제가 있었다.
반면, suppressHydrationWarning을 명시하는 방법은 hydration 경고만 숨기는 것이기 때문에 SSR의 이점을 그대로 누릴 수 있다. 다만, 이 방식은 hydration 경고를 무시하는 것이므로 실제 hydration 문제가 발생할 경우 이를 발견하기 어려울 수 있다. 또한, 이 방법은 일시적인 해결책일 뿐이므로 근본적인 hydration 문제를 해결하지는 못한다.
그럼에도 불구하고, 현재 프로젝트 상황에서는 SSR의 이점을 누리는 것이 더 중요하다고 판단하여 suppressHydrationWarning 방식을 선택했다. 하지만 향후 hydration 문제의 근본 원인을 파악하여 보다 근본적인 해결책을 모색할 예정이다.
react-quill텍스트 에디터는 기본적으로 SSR을 지원하지 않는다. 그렇기에 직접 import해오면 SSR 오류가 뜨기때문에 다른 해결방법이 필요했다.
해결방법은 Next.js에서는 dynamic import를 이용하는것이다.
Dynamic import를 사용하여 React-Quill 컴포넌트를 클라이언트 사이드에서만 로드하도록 설정함으로써, 서버 사이드에서는 해당 컴포넌트를 무시하고 클라이언트 사이드에서 렌더링이 완료된 후에만 컴포넌트를 로드하게 된다.
이 방식을 통해 SSR 오류 없이 React-Quill을 사용할 수 있었다.
먼저 추가해야할 기능 리스트다. 주변 지인들이 사용하고 피드백 준 내용을 바탕으로 리스트를 구성했다.
아래의 기능들은 시간 날때 틈틈히 구현해볼 생각이다.
- 대댓글 기능
- 태그 기능
- 동영상 업로드 가능