[인프런 워밍업 클럽 3기] 풀스택 과정 1주차 발자국 👣
1주차 주요 내용강의 및 스터디 소개SupabaseNext.jsReact QueryRecoilTailwind CSSTodo List 프로젝트 들어가며Firebase와 Flutter Web으로 개발한 서비스를 Supabase와 React.js로 (기획을 보강하여) 다시 개발하는 작업을 진행 중이다. 클라이언트 상태 관리로는 Zustand, 서버 상태 관리로는 React Query를 사용하고 CSS는 Mantine 키트를 사용하고 있다. 새로운 기술 스택이 많아 삽질도 많이 하며 작업 속도가 안 나고 있던 중에, Supabase, React Query 그리고 궁금했던 Next.js를 사용하는 강의+스터디가 있음을 알게 되어 좋은 기회라고 생각해 참여했다. 일과 병행하느라 강의를 들을 시간을 많이 확보하진 못했지만, 진득하게 앉아서 해보니 재미있게 진행했다. 강의를 통해 새롭게 알게 된 것들 위주로 정리하고, 미션 수행에서 겪었던 어려움과 회고를 정리해보려 한다. 새롭게 알게 된 것들Next.js기본 개념, 특징리액트 기반의 서버사이드 렌더링 프레임워크SSR의 이점 - 서버에서 이미 렌더링한 HTML을 검색 봇(브라우저)&사용자에게 전달하는 것이기 때문에SEO에 유리: 모든 페이지의 html 마크업이 서버에서 생성 → 검색 엔진이 페이지 인식&색인하기 유리CSR(Client Side Rendering)이 SEO에 불리한 이유는: JavaScript가 실행되기 전에는 빈 HTML 파일에 불과하기 때문페이지 로딩 속도 개선HTTP API 구현 가능 - route.ts 파일에!route.ts 파일의 코드는 웹에서 접근 불가: 서버에서 돌아가는 코드이기 때문 ⇒ DB 접속 등 보안 중요한 코드 여기서 넣으면 더 안전browser api (window, navigation, 리액트 코드 등) 사용 불가 리액트와의 차이 - route 폴더명이 route와 일치 dynamic route도 가능 (폴더명을 app/movies/[id]와 같이 [] 이용)params, searchParams를 컴포넌트의 props로 받아서 컴포넌트 내에서 사용 가능폴더 내 파일의 역할이 정해져 있음page.tsx: 각 폴더(루트)의 대표 파일layout.tsx: 화면 레이아웃 정의하는 파일route.ts: HTTP API 구현하는 파일Link 태그:a 태그 대신 Link 태그 사용할 것을 권함 Server Side Redirect 말고 Client Side Routing이 동작하게 됨 Next.js가 프로젝트 안에 어떤 링크들이 있는지 수집하는 것을 도움서버 컴포넌트, 클라이언트 컴포넌트가 구분되어 존재함React Query개요데이터 fetching, caching, db와의 동기화를 돕는 클라이언트 라이브러리cf) 캐싱 = 데이터의 복사본을 저장해 동일한 데이터의 재접근 속도를 높이는 것장점페이지 이동할 때마다 다시 불러올 필요가 없는 데이터를 캐싱하여 사용 → 불필요한 API 콜 줄임클라이언트 데이터 (zustand, recoil, useState 등으로 관리)와 서버 데이터 (react query 등으로 관리) state 관리 명확히 분리 가능React Query - 관련 글 아카이브카카오 페이 테크React Query로 비동기 데이터를 처리하는 세가지 이유불필요한 코드의 감소 ( Redux의 보일러플레이트 코드)업무와 협업의 효율성을 위한 규격화된 방식 제공: useQuery, useMutation 사용 규칙이 정해져 있음사용자 경험 향상을 위한 다양한 Built-in 기능: 로딩, 에러 처리 등이 간단리액트 쿼리 개념잡기데이터의 상태최신 데이터는 fresh, 기존 데이터는 stalestaleTime은 데이터가 fresh → stale 상태로 변경되는 데 걸리는 시간, 기본값 0cacheTime은 데이터가 inactive한 상태일 때 캐싱된 상태로 남아있는 시간특정 컴포넌트가 unmount(페이지 전환 등으로 화면에서 사라질 때) 되면 사용된 데이터는 inactive상태로 바뀌고, 이때 데이터는 cacheTime만큼 유지cacheTime 이후 데이터는 가비지 콜렉터로 수집되어 메모리에서 해제cacheTime이 지나지 않았는데 해당 데이터를 사용하는 컴포넌트가 다시 mount되면, 새로운 데이터를 fetch해오는 동안 캐싱된 데이터를 보여줌React-query는 React의 ContextAPI를 기반으로 동작useQueries - Promise.all() 처럼 여러 useQuery를 한번에 실행, 배열을 리턴select 키워드로 raw data로부터 원하는 데이터 추출해 리턴하는 사용도 가능 import { useQuery } from 'react-query' function User() { const { data } = useQuery({ queryKey: ["user"], queryFn: fetchUser, select: user => user.username, }) return Username: {data} }+궁금해진 것 - React Query의 데이터 리패치 트리거 종류에는 무엇이 있는지?Recoil개요전역 상태 관리 라이브러리 ⇒ 여러 컴포넌트에서 하나의 상태를 공유/관리하는 상황에서 유용오픈 소스주요 개념atom.ts - 모든 state를 모아두는 파일 (store.ts와 유사)atom: 초기 기본 상태를 정의하는 함수selector: 기존 상태에서 파생된 상태 - 클라이언트단에서 특정 컴포넌트에 필요한 상태만 뽑아서 사용할 때 유용useRecoilState: 리액트의 useState와 유사한 형태로 사용useRecoilValue: 값을 set 할 필요는 없고 value만 필요할 경우에 사용Supabase 연동하기DB 데이터의 타입 자동 생성하기package.json의 "scripts" 부분에, "generate-types"로 시작하는 명령어를 추가, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "generate-types": "npx supabase gen types typescript --project-id [YOUR_PROJECT_ID] --schema public > types_db.ts" },project-id 뒤 [YOUR_PROJECT_ID] 부분에 프로젝트 아이디를 추가 ([] 없이 문자열 바로)프로젝트 아이디는 api settings의 url 부분에 https:// 와 .supabase 사이에 있는 문자열실행하기 위해서 supabase 로그인하기npx supabase login브라우저 열리며 코드 입력 과정이 진행됨!(안될 경우, supabase 먼저 설치하기)실행하기 - npm run generate-typestypes_db.ts 파일이 생성된 걸 확인할 수 있음!ANON KEY란?브라우저에서 동작할 때 누구나 사용할 수 있는 API 키 값 => 이 키를 사용할 땐 누구나 사용 가능하게 설정되어 있는 API들에만 접근 가능cf) SERVICE_ROLE은 서버쪽에서 사용하는 값, secret으로 유지해야 하므로 환경변수명에 public 포함하지 않을 것 미들웨어란?새로고침이나 API 호출을 하는 등의 Request를 하고 Response를 받는 과정 중간에 추가 작업을 넣고 싶거나 쿠키를 세팅하는 등의 중간 단계를 거치고 싶을 때 인터셉트를 할 수 있게 돕는 것 미션 수행깃허브 레포지토리 링크도 함께 첨부한다!어려움미션을 수행함에 있어서 예상치 못한 난제가 있었는데, Material Tailwind 라이브러리 세팅 과정에서 버전 이슈로 충돌이 나는지 무한히 무언가 실행되려고 하는 듯했다.Error: Maximum call stack size exceeded Call Stack 55 Hide 45 ignore-listed frames Array.forEach (0:0) ...강의에서 알려주신 대로 Next.js 프로젝트를 만들었는데도 tailwind.config.ts 파일이 기본으로 없어서 직접 추가했고, tailwind css를 import 구문에서 계속 찾지 못한다고 해 uninstall과 install을 몇번을 했는데도 해결되지 않았다. 결국 tailwind 4에서 3으로 다운그레이드도 시도해보고, 다른 분들이 올린 소스코드를 참고해 package.json을 수정하는 등 여러 방법을 시도했으나, 속시원히 해결하지 못했다.시간이 많이 남진 않았어서 Tailwind CSS만 사용해 스타일링을 진행하기로 결정했다. Supabase, React Query, Next.js 사용법을 익히는 것이 지금 당장 더 중요했기 때문이다. 아 그리고 잊고 있었는데 하나 더 있었다. 기본으로 사용하는 크롬 브라우저에 익스텐션을 여러 개 깔아뒀는데, 지금 보고 있는 화면의 문서를 건드리는 것들도 있어서 그런지 로컬호스트로 실행했을 때 Hydration 관련 오류도 났었다. 조금 헤매다가 익스텐션 때문일 수 있단 답을 얻고 아무런 설정 없는 계정으로 브라우저를 열어 개발했다. 회고수행하며 느낀 점도 정리해보겠다. 우선 FontAwesome을 오랜만에 다시 썼는데 굉장히 편리했다. 쉽게 임포트할 수 있어서 간단한 프로젝트에선 이것을 사용하는 게 좋겠다. 플러터 백그라운드여서 타입을 명시해 개발하는 걸 선호하는데, 타입스크립트에서 타입 명시하는 방식도 감을 잡아가고 있다. 그래서 강의 내용에서 보충해 타입을 추가해서 작업을 해보았다. 또 React Query 사용하는 코드들은 UI, Todo 컴포넌트와 분리하여 services 내에 따로 파일을 추가했고 각 컴포넌트에서 임포트하여 사용했다.export function updateTodoMutation() { return useMutation({ mutationFn: ({ todo }: { todo: TodoRowUpdate }) => updateTodo({ todo }), onSuccess: () => { // 모든 todos 쿼리 무효화 queryClient.invalidateQueries({ queryKey: ["todos"], }); }, onError: (error) => { console.error("Error updating todo:", error); throw error; }, }); }import { deleteTodoMutation, updateTodoMutation } from "services/todo_queries"; const updateTodo = updateTodoMutation(); const deleteTodo = deleteTodoMutation(); const handleUpdate = ({ newTitle, newCompleted, }: { newTitle?: string; newCompleted?: boolean; }) => { updateTodo.mutate( { todo: { id, title: newTitle ?? title, completed: newCompleted ?? completed, completed_at: newCompleted ? new Date().toISOString() : null, }, }, { onSuccess: () => { // updateTodoMutation의 onSuccess 콜백과 함께 실행할 함수 // 컴포넌트 내부 상태관리! setIsEditing(false); }, } ); };completed 값은, 클릭 이벤트 시점의 값으로 setState를 해주고 state 값으로 업데이트하려 했을 때 정확한 상태값이 저장되지 않아서 처리 코드를 보완해줬다.onChange={(e: React.ChangeEvent) => { const newCompleted = e.target.checked; // 정확한 값 업데이트를 위해 setCompleted(newCompleted); // promise를 반환하지 않아 await해도 작업 완료까지 기다리지 않음 handleUpdate({ newCompleted }); }}일을 하며 알아낸 방법들과 강의에서 소개해준 방법들을 조합하여 간단한 프로젝트를 진행하는 거라 재미있었다. 초기 Material Tailwind 세팅에서 헤맸던 순간은 좀 고통스러웠지만.. 추가 미션인 completed_at을 저장하는 것은 위에 첨부한 코드 스니펫에서 보이는데, 서버에 저장할 completed의 값이 true이면 그 시간 정보를, false이면 null을 저장하도록 했다.투두 추가한 시간 보여주는 것도 간단하게 세로로 배치했다. {title} {todo.created_at && ( {new Date(todo.created_at).toLocaleString()} )} 사실 우리 서비스가 Todo를 다루는 거라 굉장히 익숙한 주제여서 더 재밌게 했던 것 같다. 다음 주부터는 모르는 개념이 더 많을텐데, 화이팅해서 완주하고 많이 배워가고 싶다. 화이팅.