🔥딱 8일간! 인프런x토스x허먼밀러 역대급 혜택

블로그

자바스크립트 그리고 소프트웨어 개발

저는 "리액트 훅" 강의를 만든 아저씨입니다. 자바스크립트가 이렇게 강력해졌다는 사실에 감탄했습니다. 그리고 제 생각을 옮겨보려고 합니다. 어쩔 수 없이 제 나이와 인터넷 역사가 나옵니다. 꼰대로 보이기는 싫지만 변천사이니 옛날 얘기를 조금 하겠습니다. 김대중 정부는 전국 인터넷 망을 깔았습니다. 그 후 PC방이 생겼고 저는 이메일 주소라는 걸 만들었습니다. 이메일 주소 만들기가 한시간 수업 주제였습니다. 모든 기관과 기업은 홈페이지를 만들었습니다. 홈페이지는 단순했습니다. 그림 몇개 띄우고 정보글을 올렸습니다. 홈페이지와 소프트웨어는 다른 개념이었습니다. 소프트웨어는 사람들이 일을 할 때 쓰는 도구였습니다. 홈페이지는 홍보나 검색할 때 잠깐 보는 매체였습니다. 그런데 지금은 홈페이지가 곧 소프트웨어가 되었습니다. 스프링 프레임워크에서는 "클라우드 네이티브 소프트웨어" 라는 용어를 만들었습니다. 진정 클라우드에서 작동하는 소프트웨어입니다. 그냥 정보를 보던 웹 페이지가 일을 하는 도구인 소프트웨어를 완전히 대체했다는 이야기로 이해됩니다. 아직 저도 이런 얘기가 익숙하지 않습니다. 그래도 웹페이지에서 도는 게 로컬 컴퓨팅으로 돌리는 것 보다 뭔가 약하거나 느릴거라는 의심이 듭니다. 하지만 유명 데이터 분석 도구를 만드는 Palantir 가 만든 모든 소프트웨어는 웹에서 동작합니다. 이런 변화가 가능했던 이유는 자바스크립트의 발전 때문입니다.  리액트 강의를 만들면서 자바스크립트가 ui 프로그래밍을 엄청 쉽게 만들었다는 것에 감탄했습니다. 회사들은 소프트웨어를 점점 클라우드화 시키고 있습니다. 유지보수, 배포, 과금 처리가 쉽기 때문입니다. 앞으로 자바스크립트 발전이 더 기대됩니다.  

프론트엔드jsreactjavascript

hee j

[인프런 워밍업 클럽 3기 풀스택 ] 2주차 발자국

목차Dropbox Clone ProjectDrag & Drop 할 영역 설정 및 서버에 파일 전송supabase의 Storage에 첨부파일 업로드첨부파일 검색, 삭제 2주차 미션파일의 마지막 수정(업로드) 시간을 표시하기파일명을 UUID로 변경하여 업로드하기Dropbox Clone Project1. Drag & Drop 할 영역 설정Drag & Drop 라이브러리 설치npm i --save react-dropzonefile-dragdropzone.tsx 파일div: 파일을 받는 영역 태그input: 파일 정보를 받는 태그isDragActive: 어떤 파일을 드래그 앤 드롭할 때 영역에 무엇을 보여줄지 정할 수 있게 해주는 값 ex) 드래그를 했다면? 파일을 여기에 드롭: 아니라면 파일을 드래그 앤 드롭을 해라 라는 문구 출력formData에 파일 이름과 파일 정보를 담아서 전송multiple을 true로 작성하여 여러 파일을 받을 수 있게 한다 export default function FileDragDropZone() { const uploadImageMutation = useMutation({ mutationFn: uploadFile, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["images"], // images로 시작하는 것들 전부 리셋 }); }, }); const onDrop = useCallback(async (acceptedFiles) => { // 10개 이하의 파일만 업로드함 if (acceptedFiles.length > 0 && acceptedFiles.length <= 10) { const formData = new FormData(); acceptedFiles.forEach((file) => { formData.append(file.name, file); }); await uploadImageMutation.mutate(formData); } }, []); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: true, }); return ( <div {...getRootProps()} className="w-full border-4 border-dotted border-blue-700 flex flex-col items-center justify-center py-20 cursor-pointer" > <input {...getInputProps()} /> {uploadImageMutation.isPending ? ( <Spinner /> ) : isDragActive ? ( <p>파일을 놓아주세요.</p> ) : ( <p>파일을 여기에 끌어다 놓거나 클릭하여 업로드 하세요.</p> )} </div> ); }  2. supabase의 Storage에 첨부파일 업로드.env 파일에 Storage 명 추가NEXT_PUBLIC_STORAGE_BUCKET=miniboxroot/actions 폴더 생성storageActions.ts 파일에 storage에 업로드할 함수 작성export async function uploadFile(formData: FormData) { const files = Array.from(formData.entries()).map( ([name, file]) => file as File ); // all(): 여러 파일을 한 번에 업로드 진행하기 위해 사용 const results = await Promise.all( files.map((file) => { supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(file.name, file, { upsert: true }); }) ); return results; 3. 첨부파일 검색, 삭제storageActions.ts 파일에 파일 검색, 파일 삭제 함수 추가export async function searchFiles(search: string = "") { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .list(null, { search }); // path, options, parameters handleError(error); return data; } export async function deleteFile(fileName: string) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .remove([fileName]); handleError(error); return data; } 2주차 미션[미션 완성 이미지][파일의 마지막 수정(업로드) 시간을 표시하기]dropbox-image.tsxstorage에서 받아오는 정보 내에 마지막 수정 시간을 담고 있는 updated_at을 가져와 표시formData로 날짜와 시간의 가독성을 높여줌const formData = (dateString: string) => { return format(new Date(dateString), "yyyy-MM-dd HH:mm:ss"); }; return ( ... {/* FileName */} <div>{image.name}</div> <div>수정된 시간: {formData(image.updated_at)}</div> ... ) [파일명을 UUID로 변경하여 업로드하기]- 한글명 파일이 업로드 되지 않아 uuid로 파일 명을 변경하여 업로드 해보았음- 업로드는 잘 되나 같은 파일을 업로드 하여도 파일 명이 변경되어 업로드 되기 때문에 새로 업로드 되어 업로드 시간이 변경되지 않아 적용하지는 않음uuid 설치하기npm install uuidstorageActions.tsext 변수에 첨부 파일의 확장자 추출fileName 변수에 uuid+.확장자를 합쳐 파일명 생성const results = await Promise.all( files.map((file) => { const ext = file.name.split(".").pop(); // 확장자 추출 const fileName = `${uuidv4()}.${ext}`; // UUID 기반 파일명 생성 supabase.storage .from(process.env.NEXT_PUBLIC_STORAGE_BUCKET) .upload(fileName, file, { upsert: true }); }) );

풀스택풀스택next.jsreactsupabase워밍업발자국

hee j

[인프런 워밍업 클럽 3기 풀스택 ] 1주차 발자국

목차Next.js, TailwindCss, Recoil, supabase 특징Todo List 미션  Next.jsReact 기반의 풀스택 프레임워크SSR(서버사이드 렌더링) 지원서버에서 미리 HTML을 렌더링하여 SEO와 초기 로딩 속도 개선fetch 등의 API 요청을 서버에서 처리해 클라이언트의 부담 감소SSG(정적 사이트 생성) 지원빌드 시 HTML을 미리 생성하여 빠르게 페이지 로딩파일 기반 라우팅pages/ 폴더 내 파일이 자동으로 라우트 됨app/ 디렉토리에서는 레이아웃 공유 & 동적 라우팅 가능서버 컴포넌트 & 클라이언트 컴포넌트 지원 API Routes 제공백엔드 서버 없이 NextJS 내에서 API 구축 가능이미지 최적화<Image /> 컴포넌트를 사용하면 자동으로 이미지 크기 조절 & 포맷 변환웹페이지 속도 향상SEO 최적화Middleware 지원요청이 처리되기 전에 인증, 리디렉션, 캐싱 등 제어 가능   TailwindCss유틸리티 퍼스트 방식의 CSS 프레임워크미리 정의된 클래스를 조합하여 빠르게 스타일 적용하기 때문에 CSS를 직접 작성할 필요 없음 => 개발 속도 향상 RecoilFacebook에서 개발한 React 전역 상태 관리 라이브러리React의 Context API보다 강력하고, Redux보다 간단하게 사용할 수 있음간단한 전역 상태 관리useState 처럼 쉽게 사용할 수 있음Atom을 이용해 상태를 관리하고 여러 컴포넌트에서 공유 가능 비동기 상태 관리 지원Selector를 이용하면 useEffect 없이도 비동기 데이터 관리 가능Redux보다 가볍고 React의 상태 관리 방식과 유사해 학습 부담이 적음   supabase오픈 소스 백엔스 서비스로 Firebase의 대체제로 사용됨PostgreSQL 기반의 데이터베이스RDBMS 기능 제공JSONB 데이터 타입 지원인증 권한 관리이메일, OAuth(Google, GitHub 등), Magic Link 로그인 지원 Row-Level Security(RLS) 를 통해 사용자별 데이터 접근 제한 가능스토리지 제공이미지, 파일 업로드 가능접근 권한을 설정해 보안 유지서버리스 함수(Edge Functions) 지원서버리스 API 생성 가능TypeScript와 호환Firebase 보다 쉬운 SQL 기반 데이터 관리  1주차 미션 - TODO List 제작 미션Next.js + Supabase 기반의 TODO List 제작생성 날짜와 수정 날짜 표시하기 TODO list 만들기생성 날짜와 수정 날짜 표시하기날짜를 "yyyy-MM-dd HH:mm:ss" 형태로 보여주기 위해 date-fns 설치 npm install date-fnscreated_at과 updated_at 값을 각각 상태로 관리하고,updated_at 값이 있다면 "수정됨:"이라는 텍스트와 함께 표시하고, 없다면 created_at 날짜만 표시// todo.tsx export default function Todo({ todo }) { const [created_at] = useState(todo.created_at); const [updated_at] = useState(todo.updated_at); const formatDate = (dateString: string) => { return format(new Date(dateString), "yyyy-MM-dd HH:mm:ss"); }; return <span className="text-sm text-gray-500"> {updated_at ? "수정됨: " + formatDate(updated_at) : formatDate(created_at)} </span>   

풀스택풀스택next.jsreactsupabase워밍업발자국

merry

풀스택 - 1주차

강의 수강supabase 장점오픈소스 프로젝트 (자체 서버구축 가능)PostgreSQL 기반 (관계형 DB 장점을 살릴 수 있다)Firebase 대비 저렴다양한 연동방식 지원NEXT.JS 특징“use server” → 서버에서만 동작함. (DB를 연결해도 됨)fetch + REST API 조합 api/route.ts를 사용하여 api를 만들수 있음. Server Actions api 구현을 하지않아도 데이터를 구현할 수 있다. SEO를 위한 Metadata 투두리스트 CRUD 기능 구현actions/todo-action.ts"use server"; // Next.js의 서버 액션(Server Action)으로 사용하기 위해 명시해야 함. import { Database } from "types_db"; import { createServerSupabaseClient } from "utils/supabase/server"; // 📝 todo 테이블에서 조회할 때 반환되는 데이터의 타입 export type TodoRow = Database["public"]["Tables"]["todo"]["Row"]; // 📝 todo 테이블에 새로운 데이터를 추가할 때 필요한 데이터의 타입 export type TodoRowInsert = Database["public"]["Tables"]["todo"]["Insert"]; // 📝 todo 테이블에서 기존 데이터를 수정할 때 사용할 데이터의 타입 export type TodoRowUpdate = Database["public"]["Tables"]["todo"]["Update"]; // 🔹 에러 핸들링 함수 function handleError(error: any) { console.error(error); // 에러 로그 출력 throw new Error(error.message); // 에러 메시지를 포함한 예외 발생 } export async function getTodos({ searchInput = "" }) { const supabase = await createServerSupabaseClient(); // 🔹 Supabase를 통해 todo 테이블에서 데이터 조회 const { data, error } = await supabase // ✅ `await` 추가하여 비동기 데이터 처리 .from("todo") // 📌 todo 테이블에 접근 .select("*") // 📌 모든 컬럼 조회 .like("title", `%${searchInput}%`) // 📌 LIKE 연산자로 title에 searchInput이 포함된 데이터 필터링 .order("created_at", { ascending: true }); // 📌 생성 날짜 기준 오름차순 정렬 if (error) { handleError(error); // 에러 발생 시 핸들링 } return data; // 조회된 데이터 반환 } export async function createTodo(todo: TodoRowInsert) { const supabase = await createServerSupabaseClient(); // ✅ Supabase 서버 클라이언트 생성 // 🔹 Supabase를 사용하여 todo 테이블에 새로운 데이터 삽입 const { data, error } = await supabase .from("todo") // 📌 todo 테이블에 접근 .insert({ ...todo, // 📌 클라이언트에서 받은 데이터(todo) 삽입 created_at: new Date().toISOString(), // 📌 클라이언트에서 전달된 값이 이상할 수 있으므로, 서버에서 직접 현재 시간을 생성하여 추가 }); if (error) { handleError(error); // 🔹 에러 발생 시 핸들링 } return data; // ✅ 삽입된 데이터 반환 } //todo 업데이트 export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), }) .eq("id", todo.id); // 📌 id가 일치하는 행만 업데이트 if (error) { handleError(error); } return data; } //todo 삭제 export async function deleteTodo(id: number) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase.from("todo").delete().eq("id", id); if (error) { handleError(error); } return data; } new Date().toISOString() 이란? 현재 시간을 ISO 8601 형식의 문자열로 변환하는 메서드입니다.1⃣ 시간 표준화 (Time Standardization)문제: 서버와 클라이언트의 시간대가 다를 수 있음사용자가 다른 시간대(한국, 미국, 유럽 등)에서 앱을 사용할 수 있음.예를 들어, 한국(KST)에서 저장한 날짜가 미국(PST)에서 보면 이상하게 보일 수 있음.해결책:모든 시간을 UTC 기준으로 저장하여 시간대를 통일하면 문제 해결!new Date().toISOString()은 항상 UTC(세계 표준시)로 변환되므로, 서버-클라이언트 간 시간 오차 없이 일관성 유지 가능.2⃣ 클라이언트와 서버 간 시간 차이 해결문제: 클라이언트에서 new Date()를 사용하면 각 기기의 로컬 시간이 저장됨만약 한국(KST)에서 new Date()를 저장하면 2025-03-02T23:30:45미국(PST)에서 new Date()를 저장하면 2025-03-02T06:30:45서버에서 보면 시간이 뒤죽박죽이 됨 😵해결책:모든 시간을 UTC 기준으로 저장(new Date().toISOString())클라이언트가 받을 때 필요한 시간대로 변환해서 사용예시:js 복사편집 const utcTime = new Date().toISOString(); console.log(utcTime); // "2025-03-02T14:30:45.123Z" (UTC) 한국(KST)에서는 2025-03-02 23:30:45로 변환해서 보여주면 됨.미국(PST)에서는 2025-03-02 06:30:45로 변환해서 보여주면 됨.3⃣ 데이터베이스와의 호환성문제: DB에서 created_at 필드를 올바르게 저장하려면?Supabase(PostgreSQL)에서는 TIMESTAMP 또는 TIMESTAMP WITH TIME ZONE 타입을 사용함.클라이언트에서 로컬 시간을 넣으면, DB에서 잘못 해석할 수 있음.해결책:ISO 8601 형식(2025-03-02T14:30:45.123Z)을 사용하면 DB에서 자동 변환 가능!Supabase에서 created_at이 TIMESTAMP 필드라면, new Date().toISOString()을 넣으면 자동으로 올바른 값이 저장됨.  미션TODO 항목 옆에 생성시간을 표시하기 테이블에서 생성시간을 뽑아오니 utc기준으로 보여지고 있었다. 한국시간으로 바꾸기위해 day.js 라이브러리를 활용하였다. 🤟🏻 created_at(UTC)을 로컬 시간으로 변환 로컬 시간(Local Time)이란 현재 사용자의 컴퓨터(브라우저) 또는 서버에서 설정된 시간대(Time Zone)에 맞는 시간을 의미**npm install dayjs** <p>{dayjs(todo.created_at).format("YYYY-MM-DD HH:mm:ss")}</p>  completed_at 필드를 추가하여 완료한 시간도 함께 저장하기supabase에서 compledte_at 컬럼 추가터미널에서 npm run generate-types 입력하면 types_db.ts 에 타입정보가 뜬다. completed_at 타입 추가하면 완성.//todo 업데이트 export async function updateTodo(todo: TodoRowUpdate) { const supabase = await createServerSupabaseClient(); const { data, error } = await supabase .from("todo") .update({ ...todo, updated_at: new Date().toISOString(), completed_at: new Date().toISOString(), //추가 }) .eq("id", todo.id); // 📌 id가 일치하는 행만 업데이트 if (error) { handleError(error); } return data; } 검색 입력창 디바운스 적용하여 렌더링 개선.export default function UI() { const [searchInput, setSearchInput] = useState(""); const [debouncedSearch, setDebouncedSearch] = useState(searchInput); // 검색어 입력 시 `debounce` 적용 (100ms 딜레이) useEffect(() => { const handler = setTimeout(() => { setDebouncedSearch(searchInput); }, 100); return () => clearTimeout(handler); }, [searchInput]); const { data: todosQuery, isPending, refetch, } = useQuery({ queryKey: ["todos", debouncedSearch], // 검색어가 변경될 때마다 쿼리 새로 실행 queryFn: () => getTodos({ searchInput: debouncedSearch }), }); 회고퇴근 후 매일 1시간씩 꾸준히 공부한 내 자신을 칭찬하고 싶다. 완강을 목표로 하고 있지만, 한 단계 더 나아가 다양한 기능을 직접 구현하며 부딪혀 보고 익히고 싶다. 이를 통해 한 달 후에는 스스로 미니 프로젝트를 완성해 보는 것이 목표다.시간이 날 때 조금 더 개선해 보고 싶은 사항:리액트 훅을 hooks 폴더로 분리 → 비즈니스 로직을 컴포넌트에서 분리하여 코드의 가독성과 재사용성을 높이기리액트 쿼리 키를 별도로 관리 → 쿼리 키를 체계적으로 관리하여 유지보수성과 확장성을 강화하기

풀스택supabasenext.jsreact미션

채널톡 아이콘