기술은 나의 도구
Next.js (14.1.0) 캐싱에 대하여
원문 Caching in Next.js Next.js 는 렌더링 작업 및 데이터 요청을 캐싱하여 어플리케이션 성능을 개선할 뿐만 아니라 비용을 줄여줍니다. 이 포스팅에서는 Next.js 캐싱 메커니즘과 캐싱을 구성하는데 사용할 수 있는 API, 그리고 서로 상호작용하는 방식에 대해 자세히 살펴보도록 하겠습니다. 개요 기본적으로 Next.js 는 성능개선과 비용절감을 위해 최대한 캐싱합니다. 선택적으로 캐시를 사용하지 않기로 결정하지 않은 한 라우트는 정적으로 렌더링되고 데이터 요청은 캐싱이 된다는 의미입니다. 아래 다이어그램은 빌드 시 라우트가 정적으로 렌더링되고 정적 라우트에 최초 진입했을 때 기본 캐싱 동작을 보여줍니다. 캐싱동작은 라우트가 어떻게 렌더링되는지 (정적, 동적), 데이터가 캐시되는지 아닌지 여부, 요청이 초기 방문의 일부인지 후속탐색의 일부인지에 따라 달라집니다. 사용 케이스에 따라 개별 라우팅과 데이터 요청에 대한 캐싱 동작을 다르게 구성할 수 있습니다. Request Memoization React 는 동일한 URL과 옵션을 가진 요청을 자동으로 메모이제이션하여 fetch API 를 확장합니다. React 컴포넌트 트리의 여러 곳에서 fetch 함수를 호출한다 하더라도 내부적으로는 메모이제이션하여 한번만 요청을 함으로써 동일한 데이터를 가져올 수 있다는 뜻입니다. 예를 들어, 같은 경로(예: 레이아웃, 페이지 및 여러 컴포넌트)에서 동일한 데이터를 사용해야 하는 경우 트리의 최상단에서 props 으로 하위 컴포넌트에 전달할 필요가 없습니다. 동일한 데이터를 네트워크를 통해 다중 요청하는 것에 대한 성능 영향에 대한 걱정없이 데이터를 요청할 수 있습니다. async function getItem() { // fetch 함수는 자동으로 결과를 메모하고 캐시합니다. const res = await fetch('https://.../item/1') return res.json() } // getItem 함수는 두번 요청되지만 한번 실행됩니다. const item = await getItem() // cache MISS (cache 에 없는 상태) // 두번째 요청은 같은 경로의 어디에서든 가능합니다. const item = await getItem() // cache HIT (cache 에 있는 상태) Request Memoization 이 동작하는 법 경로가 렌더링 되는 동안 특정 요청이 처음 호출되면 그 결과는 메모리에 있지 않고 cache MISS 상태일 것입니다. 그러므로 함수는 실행이 되고 외부소스에서 데이터를 가져와서 메모리에 저장합니다. 동일한 경로에서 같은 함수가 요청이 되면 cache HIT 상태이므로 함수를 실행하지 않고 메모리로 부터 데이터를 전달받습니다. 경로가 렌더링 되고 렌더링 패스 (화면에 구성요소가 그려지는 과정) 가 완료되면 메모리는 리셋되고 모든 요청 메모이제이션 항목이 지워집니다. 알아두면 좋아요 Request Memoization 은 Next.js 가 아닌 React 의 기능입니다. 다른 캐싱 메커니즘과 어떻게 상호작용하는지 보여주기 위해 여기에 포함시켰습니다. Memoization 은 fetch 요청의 GET 메소드에만 적용됩니다. Memoization 은 React 컴포넌트 트리에만 적용됩니다. generateMetadata, generateStaticParams, Layouts, Pages, 그리고 Server Components 에는 적용되지만 React 컴포넌트 트리의 일부가 아닌 Router Handler 에는 적용되지 않습니다. fetch 가 적합하지 않은 경우 (일부 Database, CMS Clients, GraphQL clients 등) React cache function 으로 함수를 메모할 수 있습니다. Duration 캐시는 React component tree 렌더링이 끝나는 서버요청 수명동안 지속합니다. Revalidating Memoization 은 서버요청 간에 공유되지 않고 렌더링되는 동안만 적용이 되기때문에 revalidate 할 필요가 없습니다. Opting out fetch 요청에서 memoization 을 하지 않기 위해서는 AbortController signal 을 요청에 넘기면 됩니다. const { signal } = new AbortController() fetch(url, { signal }) Data Cache Next.js 는 서버 요청, 배포를 통해 들어오는 데이터 결과를 유지하는 Data Cache 가 내장되어 있습니다. 이는 Next.js 가 native fetch API 를 확장하여 서버 상의 각 요청이 자체 영구 캐싱 규칙을 설정할 수 있게 함으로써 가능합니다. 알아두면 좋아요 브라우저에서 fetch 의 cache 옵션은 어떻게 요청이 브라우저의 HTTP cache 와 상호작용할지를 지시하지만 Next.js 에서는 어떻게 서버측 요청이 서버의 Data Cache 와 상호작용할지를 지시합니다. 기본적으로 fetch 를 사용하는 데이터 요청은 캐시처리됩니다. cache 나 next.revalidate 옵션으로 캐시 동작을 구성할 수도 있습니다. How the Data Cache Works 렌더링되는 동안 최초로 fetch 요청이 호출되면 Next.js 는 Data Cache 에서 캐시된 응답이 있는지 확인합니다. 캐시된 응답이 확인되면 즉시 반환되고 메모합니다. 캐시된 응답이 발견되지 않으면 데이터 소스에 요청해 결과를 Data Cache 에 저장 후 메모합니다. 캐시되지 않은 데이터가 필요하다고 설정할 경우 (e.g {cache: 'no-store'}) 결과는 항상 데이터소스에서 가져와 메모합니다. 데이터가 캐시되든, 캐시되지 않든 요청은 항상 메모하여 React 렌더링 패스가 진행되는 동안 동일한 데이터에 대한 중복요청을 피할 수 있습니다. Data Cache 와 Request Memoization 의 차이점 두 캐싱 메커니즘 모두 캐시된 데이터를 재사용하여 성능을 개선하지만 Data Cache 는 들어오는 요청과 배포를 걸쳐 영구적이며, Request Memoization 은 요청의 수명동안만 지속됩니다. Memoization 으로 동일한 렌더링 패스 안에서 렌더링 서버에서 Data Cache 서버 (e.g. CDN, Edge) 나 데이터 소스 (e.g. database, CMS) 로의 네트워크를 통한 중복 요청의 수를 줄일 수 있고 Data Cache 로 원본 데이터 소스에 대한 요청의 수를 줄일 수 있습니다. Duration Data Cache 는 revalidate 하거나 사용하지 않겠다고 선택하지 않는 이상 들어오는 요청과 배포에 걸쳐 영구적입니다. Revalidating 캐시된 데이터는 두가지 방식으로 재검증할 수 있습니다. Time-based Revalidation: 일정 시간이 지난 후 새로운 요청이 이루어지면 데이터를 재검증합니다. 자주 변경되지 않거나 최신 데이터의 중요도가 낮은 경우에 유용합니다. On-demand Revalidation: 이벤트 기반으로 데이터를 재검증합니다. (e.g. form submission) On-demand revalidation 은 태그 기반, 경로 기반 방식으로 한번에 그룹 데이터를 재검증할 때 사용할 수 있습니다. 가능한한 최신 데이터를 원할 때 유용합니다. (e.g. headless CMS 의 내용이 업데이트 될 때) Time-based Revalidation 일정한 시간 간격으로 재검증하기 위해 fetch 함수의 next.revalidate 옵션으로 리소스의 캐시 수명(초단위)을 지정할 수 있습니다. // Revalidate at most every hour fetch('https://...', { next: { revalidate: 3600 } }) 또는 특정 세그먼트 내의 모든 fetch 요청을 구성하거나 fetch 를 사용할 수 없는 경우 Route Segment Config options 을 사용할 수 있습니다. Time-based Revalidation 동작 방식 revalidate 로 최초 fetch 요청을 하면 외부 데이터소스에서 가져온 데이터가 Data Cache 에 저장됩니다. 지정한 시간 내의 모든 요청은 캐시된 데이터를 반환합니다. 지정한 시간 이후의 다음 요청도 여전히 캐시된 데이터 (now stale) 를 반환합니다. Next.js 는 백그라운드에서 데이터검증을 작동시킵니다. 데이터를 성공적으로 가져오면 Next.js 는 신규 데이터로 Data Cache 를 갱신합니다. 백그라운드 재검증이 실패하면 이전 데이터는 변경되지 않은 상태로 유지됩니다. 위의 동작은 stale-while-revalidate 와 유사합니다. On-demand Revalidation 데이터는 경로 (revalidatePath) 나 캐시 태그 (revalidateTag) 기반의 온디맨드 방식으로 재검증할 수 있습니다. On-Demand Revalidation 동작 방식 최초 fetch 요청이 호출되면 데이터를 외부에서 가져와 Data Cache 에 저장합니다. On-demand revalidation 이 작동하면 해당 캐시 항목이 캐시에서 제거됩니다. Time-based revalidation 과 다르게 동작합니다. 다음 요청에서 캐시가 발견되지 않아 (cache MISS) 데이터를 외부에서 가져와 Data Cache 에 저장합니다. Opting out 개별 데이터 요청의 cache 옵션을 no-store 로 지정할 수 있습니다. 이렇게 되면 fetch 가 호출될 때마다 데이터를 데이터 소스에서 가져옵니다. // Opt out of caching for an individual `fetch` request fetch(`https://...`, { cache: 'no-store' }) 또한 특정한 경로 세그먼트에 대해 캐싱하지 않으려면 Route Segment Config options 을 사용할 수 있습니다. 이렇게 하면 타사 라이브러리를 포함하여 경로 세그먼트의 모든 데이터 요청에 영향을 미칩니다. // Opt out of caching for all data requests in the route segment export const dynamic = 'force-dynamic' Vercel Data Cache Next.js 애플리케이션이 Vercel에 배포된 경우 Vercel Data Cache 문서를 통해 Vercel 특정 기능에 대해 더 잘 이해하는 것이 좋습니다. Full Route Cache 관련 용어 Automatic Static Optimization, Static Site Generation, Static Rendering 이라는 용어는 빌드 시 어플리케이션의 라우트를 렌더링하고 캐싱하는 프로세스를 언급할 때 혼용해서 사용될 수 있습니다. Next.js 는 빌드시점에 자동으로 라우트를 렌더링하고 캐싱합니다. 매 요청마다 서버에서 렌더링하는 대신 캐싱한 라우트를 제공하도록 최적화하여 페이지를 좀 더 빠르게 로딩할 수 있도록 합니다. Full Route Cache 가 어떻게 동작하는지 이해하기 위해서 React 가 렌더링을 어떻게 다루는지, Next.js 가 렌더링 결과를 어떻게 캐싱하는지를 살펴보는 것이 도움이 됩니다. 1. React Rendering on the Server 서버에서 Next.js 는 React API 를 사용하여 렌더링을 조율합니다. 렌더링 작업은 개별 라우트 세그먼트와 Suspense 경계에 의해 chunk 로 분할됩니다. 각 chunk 는 두 단계로 렌더링됩니다. React 는 서버 컴포넌트를 스트리밍에 최적화된 특수한 데이터 포맷으로 렌더링하는데 이를 React Server Component Payload 라고 합니다. Next.js 는 React Server Component Payload 와 Client Component Javascript instructions 를 서버에서 HTML 을 렌더링하기 위해 사용합니다. 이는 모든 작업이 렌더링될 때까지 기다릴 필요없이 응답을 스트리밍할 수 있다는 의미입니다. React Server Component Payload 란? React Server Component Payload 는 렌더링된 React Server Components tree 의 압축된 바이너리 형태입니다. React Client 에서 브라우저 DOM 을 업데이트하기 위해 사용됩니다. 다음을 포함하고 있습니다. Server Component 의 렌더링된 결과 클라이언트 컴포넌트가 렌더링될 위치에 대한 placeholder 와 해당 자바스크립트 파일에 대한 참조 Server Component 에서 Client Component 로 전달된 props 2. Next.js Caching on the Server (Full Route Cache) Next.js 에서는 서버 상에서 라우트를 렌더링한 결과 (React Server Component Payload and HTML) 를 캐싱하는 것이 기본 동작입니다. 빌드시에 정적으로 렌더링된 라우트나 재검증중인 경로에 적용됩니다. 3. React Hydration and Reconciliation on the Client 요청 시에 클라이언트에서는 .. HTML 은 Client, Server Components 의 상호작용이 없는 초기 화면을 즉시 표시하는 데 사용됩니다. React Server Components Payload 는 Client 와 렌더링된 Server Component trees 를 조정하고 DOM 을 갱신하는 데 사용됩니다. JavaScript instructions 는 Client Components 를 활성화 (hydrate) 해서 어플리케이션이 상호작용할 수 있도록 합니다. 4. Next.js Caching on the Client (Router Cache) React Server Component Payload 는 클라이언트 측 Router Cache (개별 라우트 세그먼트에 의해 분할 된 인메모리 캐시) 에 저장됩니다. Router Cache 는 이전에 방문한 경로와 프리페칭된 라우트를 저장해서 탐색 경험을 향상시킵니다. 5. Subsequent Navigations 이후의 탐색이나 프리페칭 중에 Next.js 는 React Server Components Payload 가 Router Cache 에 저장되어 있는지 확인해서 있다면 서버에 새로운 요청을 보내지 않습니다. 라우트 세그먼트가 캐시에 없는 경우 Next.js 는 서버에서 React Server Components Payload 를 가져와서 클라이언트 Router Cache 에 채웁니다. Static and Dynamic Rendering 라우트가 빌드 시에 캐시될지 아닐지는 정적, 동적 렌더링 여부에 달려있습니다. 기본적으로 정적 라우트는 캐시하고 반면에 동적 라우트는 요청 시에 렌더링 되며 캐시되지 않습니다. 아래 다이어그램은 캐시된 데이터와 캐시되지 않은 데이터를 사용하여 정적으로 렌더링된 라우트와 동적으로 렌더링된 라우트의 차이를 보여줍니다 Duration 기본적으로 Full Route Cache 는 영구적으로 지속됩니다. 렌더링 결과물이 사용자의 요청으로 캐시된다는 의미입니다. Invalidation Full Route Cache 를 무효화할 수 있는 두가지 방법이 있습니다. Revalidating Data: Data Cache 를 재검증하여 서버에서 컴포넌트를 다시 렌더링하고 새로운 렌더링 출력을 캐싱함으로써 Router Cache 를 무효화합니다. Redeploying: 배포간에 지속되는 Data Cache 와 다르게 Full Route Cache 는 신규 배포시 초기화됩니다. Opting out Full Route Cache 를 선택하지 않는 법, 다시 말해 들어오는 매 요청마다 동적으로 컴포넌트를 렌더링 하는 법은 다음과 같습니다. Dynamic Function 사용: 이렇게 하면 Full Route Cache 에서 경로를 선택 해제하고 요청 시점에 동적으로 렌더링합니다. Data Cache 는 계속 사용할 수 있습니다. dynamic = 'force-dynamic' or revalidate = 0 route segment config options: 이렇게 하면 Full Route Cache 와 Data Cache 를 건너뜁니다. 즉, 서버로 들어오는 모든 요청에 대해 컴포넌트가 렌더링되고 데이터를 가져옵니다. Router Cache 는 클라이언트 측 캐시이므로 계속 적용됩니다. Data Cache 사용하지 않기: 캐시되지 않은 라우트 요청이 있는 경우 Full Route Cache 에서 해당 경로를 제외합니다. 특정 요청에 대한 데이터는 모든 요청에 대해 fetch 됩니다. 캐싱하도록 설정된 요청은 여전히 데이터 캐시에 캐시됩니다. 이렇게 하면 캐시된 데이터와 캐시되지 않은 데이터를 혼합하여 사용할 수 있습니다.
- #Nextjs
- #Caching
homebrew mysql 설치 및 사용자 생성 과정 (MAC M1)
mysql 설치 및 실행 # 설치 (현재 기준 8.3.0 버전) $ brew install mysql # 실행 $ brew services start mysql # 로그인 $ mysql -u root 로그인 시 mysql.sock 에러가 발생한다면? Can't connect to local MySQL server through socket '/tmp/mysql.sock' $ cd /opt/homebrew/Cellar/mysql/8.3.0/support-files $ mysql.server start 사용자 생성 (mysql console) # localhost 만 허용 mysql> create user 'newuser'@'localhost' identified by 'password'; # 모든 호스트 허용 mysql> create user 'newuser'@'%' identified by 'password'; # 권한 부여 mysql> grant all privileges on *.* to 'newuser'@'localhost'; # 사용자 삭제 mysql> drop user 'newuser'@'localhost'; # 변경 사항 적용 mysql> flush privileges;
- #mysql
- #homebrew
Stable Diffusion WebUI 실행하기 (Apple Silicon)
레퍼런스 https://github.com/AUTOMATIC1111/stable-diffusion-webui/wiki/Installation-on-Apple-Silicon 실행방법 아래는 python 이 설치되어 있다는 가정하에 작성된 명령들입니다. ## 소스 다운로드 $ cd ~/Documents/Project/ $ git clone https://github.com/AUTOMATIC1111/stable-diffusion-webui.git $ cd stable-diffusion-webui ## virtual 환경 실행 $ python3 -m venv ./env $ source env/bin/activate ## 패키지 설치 $ pip install -r requirements.txt 상단 레퍼런스의 v1-5-pruned-emaonly.ckpt 링크 혹은 허깅페이스에서 ckpt 파일을 다운로드한 다음 models/Stable-diffusion 디렉토리에 옮겨주고 루트경로에서 아래 명령을 실행합니다. $ mv ~/Downloads/v1-5-pruned-emaonly.ckpt ./models/Stable-diffusion $ ./webui.sh http://127.0.0.1:7860/ 경로의 웹페이지가 자동으로 열리는데 다운로드한 모델을 선택하고 프롬프트를 입력한 다음 Generate 를 클릭하면 이미지가 생성됩니다. 실행결과
- #Stable Diffusion
- #WebUI
Vercel AI SDK 로 나만의 ChatGPT 만들기
들어가며 Vercel 은 제가 주로 사용하는 리액트 프레임워크인 Next.js 를 만든 개발사인데요. AWS 처럼 클라우드 서비스를 제공하고 있는데 정적 사이트 빌드, 배포 및 서버리스 환경에 좀 더 특화되어 있는 것 같습니다. 간만에 접속해보니 2년 전에 빌드, 배포 테스트를 하느라고 만들어 놓은 블로그 앱이 하나 있네요. 그때도 블로그를 만드려고 시도했었나 봅니다. :) 오늘은 Vercel 에서 작년에 소개한 Vercel AI SDK 를 이용해서 OpenAI ChatGPT 를 흉내내보려고 합니다. 다음의 링크를 참고하였습니다. Introducing the Vercel AI SDK ChatGPT 만들기 시작하기 전에 먼저 인터넷에서 찾은 chatgpt svg 파일을 다운받아 public 폴더로 옮겨놓습니다. Next.js Project Setup & Install Package # Next.js 14.0.4 으로 설치됨 (Typescript, Tailwindcss, `src/` directory, App Router 사용) npx create-next-app@latest vercel-ai # Vercel AI npm i ai openai-edge # OPENAI_API_KEY 설정용 npm i dotenv # Icon npm i @fortawesome/react-fontawesome @fortawesome/free-solid-svg-icons API 키 설정 (.env) OPENAI_API_KEY=sk-xxxxxxxxxxxx dotenv 설정 (next.config.js) require("dotenv").config(); ... 이하 ... TailwindCSS 설정 (src/app/globals.css) @tailwind base; @tailwind components; @tailwind utilities; 기본 레이아웃 (src/app/layout.tsx) import type { Metadata } from "next"; import "./globals.css"; export const metadata: Metadata = { title: "Vercel AI", description: "Chat GPT with Vercel AI", }; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="ko"> <body>{children}</body> </html> ); } API (src/app/api/chat/route.ts) import { OpenAIStream, StreamingTextResponse } from "ai"; import { Configuration, OpenAIApi } from "openai-edge"; const config = new Configuration({ apiKey: process.env.OPENAI_API_KEY, }); const openai = new OpenAIApi(config); export const runtime = "edge"; export async function POST(req: Request) { const { messages } = await req.json(); const response = await openai.createChatCompletion({ model: "gpt-3.5-turbo", stream: true, messages, }); const stream = OpenAIStream(response); return new StreamingTextResponse(stream); } 화면 (src/app/page.tsx) "use client"; import { useChat } from "ai/react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faUser } from "@fortawesome/free-solid-svg-icons"; import { useEffect, useRef } from "react"; export default function Chat() { const contentRef = useRef<HTMLDivElement>(null); const { messages, input, handleInputChange, handleSubmit } = useChat(); useEffect(() => { if (contentRef.current) { contentRef.current.scrollTop = contentRef.current.scrollHeight; } }, [messages]); return ( <div className="flex flex-col justify-center items-center h-lvh"> <div className="flex justify-center items-center font-medium text-xl fixed top-0 bg-white h-16 w-full border-b-[1px] border-b-black"> ChatGPT{" "} <span className="text-gray-500 ml-2 text-lg">with Vercel AI</span> </div> <div className="w-full h-full flex flex-col justify-center items-center"> <div ref={contentRef} className="w-full h-full overflow-hidden overflow-y-scroll px-24 pt-20 pb-10" > {messages.map((m) => ( <div key={m.id} className="text-lg"> <div className="flex mt-5"> <div className="h-full mr-2 w-12 pt-[3px] min-w-12"> {m.role === "assistant" ? ( <img src="/gpt.svg" width="25" alt="gpt" /> ) : ( <FontAwesomeIcon icon={faUser} size="lg" className="pl-0.5" /> )} </div> <div> {m.role === "assistant" ? ( <strong>ChatGPT</strong> ) : ( <strong>You</strong> )} <div>{m.content}</div> </div> </div> </div> ))} </div> <form onSubmit={handleSubmit} className="flex items-center w-full h-14 bg-white p-2" > <input value={input} onChange={handleInputChange} className="w-full h-10 border border-black focus:outline-none p-2 rounded-md" placeholder="Message ChatGPT..." /> </form> </div> </div> ); } 결과물 마무리 기본적인 기능만 구현하긴 했지만 별 수고로운 작업없이 useChat hook 하나로 정리가 되어 편리하네요. 응답을 데이터베이스에 저장하도록 콜백도 제공하고 Langchain, Anthropic, Hugging Face 도 지원하니 잘 활용해 보면 손쉽게 괜찮은 결과물이 나올 수 있을 것 같습니다.
- #Vercel
- #AI
- #ChatGPT
Pycharm Jupyter Notebook 단축키 모음 (for Mac)
셀 실행 Ctrl + Enter 현재 셀 실행 Shift + Enter 현재 셀 실행 후 다음 셀로 이동 셀 추가 및 삭제 A 현재 셀 위에 셀 추가 B 현재 셀 아래에 셀 추가 D, D (연속 두번) 현재 셀 삭제 셀 복사, 잘라내기, 붙여넣기 C 현재 셀 복사 X 현재 셀 잘라내기 V 현재 셀 아래에 복사한 셀 붙여넣기 셀 병합 Shift + M 선택한 셀들을 병합 셀 타입 변경 M 현재 셀을 마크다운 타입으로 변경 Y 현재 셀을 코드 타입으로 변경 셀 편집 모드 전환 Enter 셀 편집 모드 진입 Esc 셀 편집 모드 탈출 셀 실행 중단 I, I (연속 두번) 실행 중인 셀 중단
- #Pycharm
- #Jupyter
- #Shortcut
이십년만에 개인 홈페이지 만든 썰
갑자기 홈페이지는 왜? 제가 현재 스펙의 개인 홈페이지를 만들기로 결정한 데는 매우 여러가지 이유가 있습니다. 기술에 대한 갈증도 있었지만 다른 고민도 많았거든요. 먼저 기술과 관련된 이유입니다. React Styled component가 썩 맘에 들진 않지만 익숙해지고 싶었다. CSS를 좀 익혀야 하는데 마땅한 프로젝트가 없었다. Gatsby를 사용해 보고 싶었다. GraphQL이 어떻게 동작하는지 알고 싶었다. Headless CMS의 실체를 보고 싶었다. 서버를 관리하고 운영하는 것은 신경 쓸 게 너무 많아서 더 나은 방법을 찾고 싶었다. 그리고 또 다른 이유입니다. 내 도메인을 갖고 있지만 쓰지 않고 있었다. 열심히 개발을 하면서 살아왔는데 내 Product가 없다. 하고싶은 게 많은데 실천하지 않았다. 나 자신을 브랜딩한 적이 없었다. 나 뭐하면서 살고 있나? 위와 같은 고민들은 평소에도 했었지만 일을 핑계로 항상 미루고 있었죠. 미루는 것은 참 쉽습니다. 미루기로 결정만 하면 되니까요. 근데 그게 수십(!)수년간 이어져오니 갑자기 짜증이 확 나는 겁니다. 저는 주로 백엔드 반, 프론트엔드 반 정도의 비중으로 개발을 하고 있습니다. 그 전엔 백엔드 작업의 비중이 컸는데 3년 전 쯤부터 React를 시작하면서 프론트엔드 비중이 많이 늘었습니다. 근데 최근에 백엔드 일부를 담당했던 AI 관련 프로젝트에서 프론트엔드 소스 분석이 필요해서 살펴보는데 그동안 제가 해왔던 구성 (Next.js + Redux) 과 많이 다른데다가 너무 복잡한 거예요. (React + Styled component + RTK Query) 나 React 하고 있는데 왜 이해가 안돼지? React 기반의 좀 더 확장된 개발방법에 대한 공부가 필요하다는 생각을 했고 남들은 어떻게 사용하고 있는지 궁금해졌습니다. 그러던 중 노마드코더의 React JS 마스터클래스 강의를 찾았는데 이 과정의 마지막 보너스 섹션에 (Gatsby + GraphQL + Contentful) 조합을 발견한 것이죠. 어느새 손에는 카드가 쥐어져 있었고.. 이 자리를 빌려 Nico 에게 감사하다는 말을 전하고 싶습니다. Styled component, Typescript, Framer Motion, Recoil 도 잘 배웠습니다. (노마드코더 홍보글 아님. 암튼 배울 점이 참 많아서 개발에 관심이 있으신 분들 노마드코더 강의 추천드려요.) 홈페이지를 만들 생각으로 강의를 들은 건 아니었습니다. 단순히 호기심을 해소할 목적으로 들은 강의였지만 강의를 모두 듣고 나니 써먹을 데가 생각이 난 것이죠. 그동안은 홈페이지 없이 - 옛날에도 만들긴 했었지만 너무너무 옛날임 - 이 블로그, 저 블로그 옮겨다니며 글 좀 끄적이다가 방치하기 일쑤였거든요. 마음에 드는 블로그 플랫폼도 없었고 커스텀하게 디자인, 기능을 변경하는 작업은 굉장히 참을성을 요구하는 일이었습니다. 설치형 워드프레스로도 만들어봤지만 CMS가 별로였고 서버 관리하는 것도 귀찮았습니다. 그래서 공부한 걸 익힐 겸 그냥 하나 만들자! 라고 생각하게 된 것이죠. 실천하기도 참 쉽네요. 실천하기로 결정하고 움직이면 되니. 내용은? 나의 홈페이지는 나만의 정체성으로 차 있어야 한다고 생각했기 때문에 결국 제가 좋아하는 것들로 채워지겠죠. 특정 주제로 제한하고 싶진 않아요. 그래서 책, 기술, 영감을 얻을 수 있는 글들, 음악 등의 좋아하는 카테고리들로 배치했고 마지막으로 실험실을 하나 추가했습니다. 실험실은 그동안 해보고 싶었던 것들, 새롭게 시도하는 것들로 채워지게 될 거예요. 한마디로 생각을 실천으로 옮기는 공간이 되는 것이죠. 계속 업데이트되는 로그가 될 수도 있고 어떠한 결과물이 될 수도 있습니다. 말그대로 실험실이라 실험하다가 재미없다 싶으면 없앨 수도 있겠죠. 컨텐츠가 좀 더 빌드업되면 별도로 사업화해서 운영하게 될 지도 모를 일이고요. 사실 그걸 노리고 다양한 시도를 해보려고 하는 겁니다. 뭐로 만들었어? Gatsby (정적 사이트 생성 프레임워크) React 기반의 정적 사이트 생성을 위한 프레임워크입니다. 큰 규모 웹사이트의 경우 빌드 시간이 길어질 수 있고, 동적 기능 구현에 제한이 있을 수 있지만 정적 사이트로써는 SEO 에 친화적이고 코드, 이미지가 최적화되어 개인형 블로그, 홈페이지에 매우 적합할 것으로 판단했습니다. 사용해보니 다양한 플러그인들이 있어 작업시간을 줄여주네요. 그동안 사용하던 Next.js 의 SSG(Static Site Generation)를 이용해볼까도 생각했지만 일단 먼저 경험해보고 싶었고 정적 사이트 생성으로써는 훌륭하다 생각되네요. React (JS UI 라이브러리) Facebook 에서 만든 오픈소스 JS 라이브러리입니다. 가상DOM 을 이용해 성능을 최적화하고 컴포넌트 기반이라 코드 재사용이 매우 용이합니다. 현재 프론트엔드 개발할 때 제가 주로 사용하는 라이브러리이고 Gatsby를 선택한 이유이기도 하죠. Styled Component (React CSS 라이브러리) React 애플리케이션에서 스타일을 적용하기 위한 라이브러리로 JavaScript와 CSS를 결합하는 CSS-in-JS의 접근 방식을 사용하여 컴포넌트 레벨에서 스타일을 관리할 수 있습니다. 개인적으로는 CSS 와 JS 가 결합되는 방식을 좋아하지 않았는데 props 를 활용해 조건별 스타일 분기하는 부분이 꽤 맘에 들었고 JSX 로만 보면 꽤 간결해지는 장점이 있는 것 같습니다. 모두 컴포넌트네요. React 철학에 부합하는 CSS 라이브러리가 아닌가 싶습니다. Contentful (컨텐츠 관리) 클라우드 기반의 Headless CMS 입니다. 컨텐츠를 플랫폼과 분리시켜 다양한 플랫폼에서 Rest, GraphQL API 로 제공받을 수 있는 부분이 큰 장점입니다. 컨텐츠의 성격에 맞게 모델링이 가능한 부분도 매력적이네요. 컨텐츠에 집중했지만 다양한 외부 서비스와 통합이 가능해서 좋네요. Github dispatch api 를 Webhook 으로 연결해서, 컨텐츠를 등록하거나 수정하면 Github 저장소에 즉시 배포되도록 설정해 두었습니다. GraphQL 필요한 데이터의 구조를 쿼리로 요청해 꼭 필요한 데이터만 받을 수 있어 오버페칭, 언더페칭을 줄일 수 있는 게 큰 장점이예요. Rest와 달리 단일 엔드포인트입니다. Rest만 사용하다보니 이 부분이 참 신기했어요. 익숙하지 않아서 다른 프로젝트에 GraphQL을 도입할 일이 있을지는 모르겠지만 GraphQL의 장점을 활용하기 위해 Rest를 래핑해서 GraphQL로 전환하는 것은 좀 아닌 것 같아요. Github Pages (호스팅 및 배포) Github 저장소와 직접 연결되어 정적 사이트만 호스팅이 가능하고, Git을 통한 버전 관리와 배포가 용이합니다. 소스를 Github에 push 하면 Github Action 을 통해 빌드, 배포됩니다. Cloudflare (DNS, 보안) CDN, 보안 등의 다양한 네트워크 관련 서비스를 제공하는데요. 이 홈페이지에서는 DNS 서버로 활용하고 있고 SSL, DDos 방어 등의 기본 제공 기능을 사용하고 있습니다. Github pages 만으로도 가능해서 오버스펙이긴 한데 경험이 목적이고 Cloudflare 도 잘 활용해보고 싶었어요. 마무리 홈페이지를 만들기로 작정하고 기획부터 시작한 게 아니라 무턱대고 코드를 짜기 시작했습니다. 사용할 도구들을 정하고 나니 빨리 만들고 싶었거든요. 어차피 개인 프로젝트라 일반적인 작업흐름을 따르는 것보단 제 방식대로 해보고 싶었어요. 제대로 기획, 디자인해 본 적도 없고요. CSS가 익숙하지 않은 제가 디자인 결과물없이 UI 작업을 하다보니 쉽지 않았는데요. 그래도 그 과정이 재밌었습니다. 완성본이 아니라 계속 다듬어 가고 있는 중이라 한달 뒤에 또 어떤 모습으로 바뀌어 있을지는 저도 모르겠네요. 나 자신을 위해 무언가를 만들어가고 있다는 사실이 무척 즐겁습니다. 그런 시간을 지속적으로 늘려가고 싶어요.
- #홈페이지
- #moztiq.com
[플러터] 가운데 정렬을 위한 여러가지 방법
Center 위젯 Text 위젯을 수직 및 수평으로 중앙에 배치 import 'package:flutter/material.dart'; void main() { runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: Center( child: Text( 'Center', style: TextStyle( color: Colors.black, fontSize: 30, ), ), ), ), ); } } Align 위젯 Align 위젯의 alignment 속성을 Alignment.center 로 정의한다. import 'package:flutter/material.dart'; void main() { runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return const MaterialApp( home: Scaffold( body: Align( alignment: Alignment.center, child: Text( 'Center', style: TextStyle( color: Colors.black, fontSize: 30, ), ), ), ), ); } } Container 위젯 Container 위젯의 alignment 속성을 Alignment.center 로 정의한다. import 'package:flutter/material.dart'; void main() { runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( body: Container( alignment: Alignment.center, child: const Text( 'Center', style: TextStyle( color: Colors.black, fontSize: 30, ), ), ), ), ); } } 결과물
- #플러터
- #가운데 정렬
[노마드코더 플러터] Pomodoro
Flexible Class Documentation 하위 위젯이 남은 공간을 어떻게 분배할지를 결정 하위 위젯에게 유연성을 제공하는 것이 목적 fit: Flexfit.tight 를 설정하면 Expanded 와 동일한 동작 Expanded Class Documentation Row, Column 또는 Flex 의 하위 위젯의 사용가능한 공간을 확장한다. 하위 위젯의 할당된 공간을 채우기 위한 목적 main.dart import 'package:flutter/material.dart'; import 'package:nc_flutter_pomodoro/screens/home_screen.dart'; void main() { runApp(const App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( colorScheme: ColorScheme.fromSwatch( backgroundColor: const Color(0xFFE7626C), ), textTheme: const TextTheme( displayLarge: TextStyle( color: Color(0xFF232B55), ), ), cardColor: const Color(0xFFF4EDDB), ), home: const HomeScreen(), ); } } home_screen.dart import 'dart:async'; import 'package:flutter/material.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State<HomeScreen> createState() => _HomeScreenState(); } class _HomeScreenState extends State<HomeScreen> { static const originSeconds = 5; int totalSeconds = originSeconds; bool isRunning = false; int totalPomodoros = 0; late Timer timer; void onTick(Timer timer) { setState(() { if (totalSeconds == 0) { timer.cancel(); isRunning = false; totalPomodoros = totalPomodoros + 1; totalSeconds = originSeconds; } else { totalSeconds = totalSeconds - 1; } }); } void onStartPressed() { timer = Timer.periodic( const Duration(seconds: 1), onTick, ); setState(() { isRunning = true; }); } void onPausePressed() { timer.cancel(); setState(() { isRunning = false; }); } String twoDigits(int n) => n.toString().padLeft(2, '0'); String format(int seconds) { var duration = Duration(seconds: seconds); var digitMinutes = twoDigits(duration.inMinutes.remainder(60)); var digitSeconds = twoDigits(duration.inSeconds.remainder(60)); return "$digitMinutes:$digitSeconds"; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).colorScheme.background, body: Column( children: [ Flexible( flex: 1, child: Container( alignment: Alignment.bottomCenter, child: Text( format(totalSeconds), //'25:00', style: TextStyle( fontSize: 89, fontWeight: FontWeight.w600, color: Theme.of(context).cardColor, ), ), ), ), Flexible( flex: 2, child: Center( child: IconButton( iconSize: 120, color: Theme.of(context).cardColor, icon: isRunning ? const Icon( Icons.pause_circle_outline, ) : const Icon( Icons.play_circle_outline, ), onPressed: isRunning ? onPausePressed : onStartPressed, ), ), ), Flexible( flex: 1, child: Row( children: [ Expanded( child: Container( decoration: BoxDecoration( color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(50), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Pomodoros', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: Theme.of(context).textTheme.displayLarge!.color, ), ), Text( '$totalPomodoros', style: TextStyle( fontSize: 58, fontWeight: FontWeight.w600, color: Theme.of(context).textTheme.displayLarge!.color, ), ), ], ), ), ), ], ), ), ], ), ); } } 결과물
- #노마드코더
- #플러터
[노마드코더 플러터] StatefulWidget
StatefulWidget 상태 (state) 를 갖고 있어 상태값이 변경될 때마다 UI 를 업데이트 State 클래스와 연결되어 있으며 데이터와 UI 를 관리하는 로직은 State 클래스 내에서 관리 BuildContext 에만 의존하는 경우 StatelessWidget 으로 구현 setState 상태 변경 build 메소드 호출하여 UI 업데이트 initState 한번만 호출된다. build 메소드보다 먼저 호출된다. 초기화 로직이 필요한 경우 수행할 수 있음 (초기값 설정, 데이터 로드 등) UI 가 생성되지 않은 상태 dispose widget 이 스크린에서 제거될 때 호출 (State 객체가 영구적으로 제거될 때) 사용한 리소스 해제할 로직이 필요한 경우 수행 main.dart import 'package:flutter/material.dart'; void main() { runApp(const App()); } class App extends StatefulWidget { const App({super.key}); @override State<App> createState() => _AppState(); } class _AppState extends State<App> { bool showTitle = true; void toggleTitle() { setState(() { showTitle = !showTitle; }); } @override Widget build(BuildContext context) { return MaterialApp( theme: ThemeData( textTheme: const TextTheme( titleLarge: TextStyle( color: Colors.red, ), ), ), home: Scaffold( backgroundColor: const Color(0xFFF4EDDB), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ showTitle ? const MyLargeTitle() : const Text('nothing'), IconButton( onPressed: toggleTitle, icon: const Icon(Icons.remove_red_eye)) ], ), ), ), ); } } class MyLargeTitle extends StatefulWidget { const MyLargeTitle({ super.key, }); @override State<MyLargeTitle> createState() => _MyLargeTitleState(); } class _MyLargeTitleState extends State<MyLargeTitle> { @override void initState() { super.initState(); print('initState'); } @override void dispose() { super.dispose(); print('dispose'); } @override Widget build(BuildContext context) { print('build'); return Text( 'My Large Title', style: TextStyle( fontSize: 30, color: Theme.of(context).textTheme.titleLarge!.color, ), ); } }
- #노마드코더
- #플러터
[노마드코더 플러터] Wallet UI
main.dart import 'package:flutter/material.dart'; import 'package:nc_flutter_crypto/widgets/button.dart'; import 'package:nc_flutter_crypto/widgets/currency_card.dart'; void main() { runApp(App()); } class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( backgroundColor: const Color(0xFF181818), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( height: 80, ), Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ const Text( "Hey, Selena", style: TextStyle( color: Colors.white, fontSize: 28, fontWeight: FontWeight.w800, ), ), Text( "Welcome back", style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 18, ), ), ], ) ], ), const SizedBox( height: 70, ), Text( 'Total Balance', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 22), ), const SizedBox( height: 5, ), const Text( '\$5 194 482', style: TextStyle( color: Colors.white, fontSize: 48, fontWeight: FontWeight.w600), ), const SizedBox( height: 30, ), const Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Button( textColor: Colors.black, bgColor: Color(0xFFF2B33A), text: 'Transfer', ), Button( textColor: Colors.white, bgColor: Color(0xFF6C777E), text: 'Request', ), ], ), const SizedBox( height: 70, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.end, children: [ const Text( 'Wallets', style: TextStyle( color: Colors.white, fontSize: 36, fontWeight: FontWeight.w600, ), ), Text( 'View All', style: TextStyle( color: Colors.white.withOpacity(0.8), fontSize: 18), ), ], ), const SizedBox( height: 20, ), const CurrencyCard( name: 'Bitcoin', code: 'BTC', amount: '2 342', icon: Icons.currency_bitcoin_rounded, isInverted: false, ), Transform.translate( offset: const Offset(0, -25), child: const CurrencyCard( name: 'Dollar', code: 'USD', amount: '243', icon: Icons.attach_money_rounded, isInverted: true, ), ), Transform.translate( offset: const Offset(0, -50), child: const CurrencyCard( name: 'Euro', code: 'EUC', amount: '56 234', icon: Icons.euro_rounded, isInverted: false, ), ), ], ), ), ), ), ); } } currency_card.dart import 'package:flutter/material.dart'; class CurrencyCard extends StatelessWidget { final String name, code, amount; final IconData icon; final bool isInverted; final _blackColor = const Color(0xFF1F2123); const CurrencyCard({ super.key, required this.name, required this.code, required this.amount, required this.icon, required this.isInverted, }); @override Widget build(BuildContext context) { return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), color: isInverted ? Colors.white : _blackColor, ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 30, ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: TextStyle( color: isInverted ? _blackColor : Colors.white, fontSize: 30, fontWeight: FontWeight.w600, ), ), const SizedBox( height: 10, ), Row( children: [ Text( amount, style: TextStyle( color: isInverted ? _blackColor : Colors.white, fontSize: 27, ), ), const SizedBox( width: 10, ), Text( code, style: TextStyle( color: isInverted ? _blackColor : Colors.white, fontSize: 20, ), ), ], ) ], ), Transform.scale( scale: 2.5, child: Transform.translate( offset: const Offset(5, 15), child: Icon( icon, color: isInverted ? _blackColor : Colors.white, size: 80, ), ), ) ], ), ), ); } } button.dart import 'package:flutter/material.dart'; class Button extends StatelessWidget { final Color textColor; final Color bgColor; final String text; const Button({ super.key, required this.textColor, required this.bgColor, required this.text, }); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: bgColor, borderRadius: BorderRadius.circular(40), ), child: Padding( padding: const EdgeInsets.symmetric( vertical: 20, horizontal: 50, ), child: Text( text, style: TextStyle( color: textColor, fontSize: 20, fontWeight: FontWeight.w600, ), ), ), ); } } 결과물
- #노마드코더
- #플러터
[노마드코더 플러터] Hello Flutter
Flutter 설치 SDK & Simulator 설치 $ brew install --cask flutter Android Setup iOS Setup ## 설치 진단 $ flutter doctor Project 생성 및 실행 $ flutter create hello $ cd hello $ flutter run IntelliJ 설정 Dart Plugin 설치 Flutter Plugin 설치 Dart Code Auto Format on Save iOS Simulator 열기 macOS 선택 후 프로젝트 실행 Android SDK 설치 Android Device 추가 Android Emulator Widget 레고블럭과 같다. flutter 의 모든 것은 Widget 이다. Widget 들을 합치는 방식으로 앱을 만든다. 공식 Widget Widget 을 만들기 위해서는 flutter SDK 에 있는 3개의 core Widget 중 하나를 상송받아야 한다. StatelessWidget StatefulWidget InheritedWidget StatelessWidget for widgets that always build the same way given a particular configuration and ambient state StatelessWidget Widget 을 상속받은 클래스는 build 메소드를 구현해야 한다. 이 때 build 메소드는 반드시 2개 옵션 중 하나를 return 해야 한다. material : 구글 디자인 시스템 cupertino : 애플 디자인 시스템 Scaffold 화면의 구조를 제공해 준다. 모든 화면에 필요하다. Navigation Bar, Bottom tab bar, 상단 버튼, 화면 중앙 정렬 등을 구현한다. Hello World import 'package:flutter/material.dart'; void main() { runApp(App()); // App Widget : 어플리케이션의 Root 위젯 } class App extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar(title: Text('Hello Flutter!')), body: Center( child: Text('Hello World!'), ))); } }
- #노마드코더
- #플러터
[노마드코더 플러터] 플러터 소개
Why Flutter? 진정한 의미의 크로스플랫폼 프레임워크 iOS, Android, Web, MacOS, Windows, Linux 에서 동작하는 어플리케이션 & Embedded 하나의 언어와 프레임워크로 원하는 모든 것을 만들 수 있다. How Flutter Works Architectural Layer 타 프레임워크들은 운영체제와 직접 소통하는 반면 플러터에서 작성한 코드는 운영체제와 직접적인 소통을 하지 않는다. 크로스플랫폼이 되기 위해서 다른 방식의 소통이 필요하여 Engine 을 도입 Engine 이 화면 상에 UI 를 그리는 역할을 하게 됨 (비디오 게임 엔진처럼) Flutter 어플리케이션은 운영체제의 Native Widget 을 사용하지 않음 어플리케이션의 호스트에 의존할 필요없이 (크로스플랫폼) 화면상의 모든 픽셀을 조절함으로써 모든 걸 통제할 수 있음 Embedder 엔진을 가동시키는 runner 프로젝트를 지칭 실행 순서 : Start Application → Runner → Engine → UI Rendering 정리) 플러터의 역할 단순히 Engine 을 어플리케이션 내부에 집어넣고 우리가 작성한 Dart 코드를 컴파일 사용자가 어플리케이션을 실행시킬 때 Engine 을 가동시키는 runner 프로젝트 (Embedder) 를 실행 이후 Engine 이 UI 를 렌더링 FAQ How does Flutter run my code on iOS? Does Flutter use my operating system’s built-in platform widgets? 단점 네이티브에서 가능한 위젯을 사용할 수 없어 부자연스럽다. (사람들이 Flutter 를 싫어하는 이유) → 자연스럽게 만드는 법이 존재함 Flutter vs React Native React Native 네이티브 앱 운영체제 상에서 가능한 위젯을 사용하고 싶은 경우 예를 들어 버튼을 하나 만들면 iOS 와 안드로이드에서 서로 다른 UI 로 렌더링 자바스크립트를 통해 운영체제와 소통 Flutter 아주 세밀한 디자인 요구사항이 들어가 있고 요소들이나 애니메이션들을 전부 커스터마이징해야 하는 경우 운영체제에서 제공해주는 위젯을 사용할 필요가 없는 경우
- #노마드코더
- #플러터
Dart List Method 사용법
add(E value) → void Adds value to the end of this list, extending the length by one. List<int> numbers = [1]; numbers.add(2); print(numbers); // dict : [1, 2] addAll(Iterable iterable) → void Appends all objects of iterable to the end of this list. List<int> numbers = [1, 2]; numbers.addAll([3, 4]); print(numbers); // dict : [1, 2, 3, 4] any(bool test(E element)) → bool Checks whether any element of this iterable satisfies test. 하나라도 만족하면 true 하나도 만족하지 못하면 false List<int> numbers = [1, 2, 3, 4]; print(numbers.any((el) => el > 3)); // true print(numbers.any((el) => el > 4)); // false asMap() → Map<int, E> An unmodifiable Map view of this list. List 의 인덱스를 key 로 하고 객체를 값으로 하는 Map 으로 변환 List<String> terms = ['tweet', 'fat', 'thin']; Map<int, String> newTerms = terms.asMap(); print(newTerms); // {0: tweet, 1: fat, 2: thin} cast() → List Returns a view of this list as a list of R instances. 목록의 요소를 특정 유형으로 변환 상위 유형의 목록이 있고 더 구체적인 유형의 목록이 필요할 때 사용 List<Object> dict = ["tweet", "fat", "thin"]; List<String> newDict = dict.cast<String>(); print(newDict); // ["tweet", "fat", "thin"] clear() → void Removes all objects from this list; the length of the list becomes zero. List<int> numbers = [1, 2, 3, 4]; numbers.clear(); print(numbers); // [] contains(Object? element) → bool Whether the collection contains an element equal to element. List<int> numbers = [1, 2, 3, 4]; print(numbers.contains(1)); // true print(numbers.contains(5)); // false elementAt(int index) → E Returns the indexth element. List<int> numbers = [1, 2, 3, 4]; print(numbers.elementAt(2)); // 3 every(bool test(E element)) → bool Checks whether every element of this iterable satisfies test. 모두 만족하면 true 하나라도 만족하지 못하면 false List<int> numbers = [1, 2, 3, 4]; print(numbers.every((el) => el > 0)); // true print(numbers.every((el) => el > 1)); // false expand(Iterable toElements(E element)) → Iterable Expands each element of this Iterable into zero or more elements. 일반적으로 목록의 각 요소를 반복하고 각 요소에서 다른 유형의 이터러블을 생성하는 방법 List<Map<String, String>> listOfMaps = [ {'key1': 'value1', 'key2': 'value2'}, {'key3': 'value3', 'key4': 'value4'}, ]; Iterable<String> allValues = listOfMaps.expand((map) => map.values); print(allValues.toList()); // [value1, value2, value3, value4] Iterable<int> count(int n) sync* { for (var i = 1; i <= n; i++) { yield i; } } var numbers = [1, 3, 0, 2]; print(numbers.expand(count).toList()); // [1, 1, 2, 3, 1, 2] fillRange(int start, int end, [E? fillValue]) → void Overwrites a range of elements with fillValue. List<int> numbers = List.filled(5, 1); // numbers: [1, 1, 1, 1, 1] numbers.fillRange(2, 4, 6); print(numbers); // [1, 1, 6, 6, 1] firstWhere(bool test(E element), {E orElse()?}) → E The first element that satisfies the given predicate test. List<int> numbers = [3, 5, 6, 4]; int overThree = numbers.firstWhere((el) => el > 3); print(overThree); // 5 fold(T initialValue, T combine(T previousValue, E element)) → T Reduces a collection to a single value by iteratively combining each element of the collection with an existing value List<int> numbers = [1, 2, 3, 4]; final result = numbers.fold(0, (prev, el) => prev + el); print(result); // 10 followedBy(Iterable other) → Iterable Creates the lazy concatenation of this iterable and other. List<int> numbers = [1, 2, 3, 4]; final result = numbers.followedBy([5, 6]); print(result.toList()); // [1, 2, 3, 4, 5, 6] forEach(void action(E element)) → void Invokes action on each element of this iterable in iteration order. List<int> numbers = [1, 2, 3, 4]; // case 1 numbers.forEach((el) => print(el + 1)); // case 2 numbers.forEach((el) { var result = el + 1; print(result); }); getRange(int start, int end) → Iterable Creates an Iterable that iterates over a range of elements. List<int> numbers = [1, 2, 3, 4]; final ranged = numbers.getRange(0, 3); print(ranged.toList()); // [1, 2, 3] indexOf(E element, [int start = 0]) → int The first index of element in this list. List<int> numbers = [1, 2, 3, 4, 3, 2, 1]; print(numbers.indexOf(3)); // 2 indexWhere(bool test(E element), [int start = 0]) → int The first index in the list that satisfies the provided test. List<int> numbers = [1, 2, 3, 4, 3, 2, 1]; print(numbers.indexWhere((el) => el > 2)); // 2 insert(int index, E element) → void Inserts element at position index in this list. List<int> numbers = [1, 2, 3, 4]; numbers.insert(2, 5); print(numbers); // [1, 2, 5, 3, 4] insertAll(int index, Iterable iterable) → void Inserts all objects of iterable at position index in this list. List<int> numbers = [1, 2, 3, 4]; numbers.insertAll(2, [5, 6]); print(numbers); // [1, 2, 5, 6, 3, 4] join([String separator = ""]) → String Converts each element to a String and concatenates the strings. List<String> texts = ["my","name","is","moz"]; print(texts.join(" ")); // my name is moz lastIndexOf(E element, [int? start]) → int The last index of element in this list. List<int> numbers = [1, 2, 3, 4, 3, 2, 1]; print(numbers.lastIndexOf(2)); // 5 lastIndexWhere(bool test(E element), [int? start]) → int The last index of element in this list. List<int> numbers = [1, 2, 3, 4, 3, 2, 1]; print(numbers.lastIndexWhere((el) => el > 2)); // 4 lastWhere(bool test(E element), {E orElse()?}) → E The last element that satisfies the given predicate test. List<int> numbers = [5, 6, 7, 4, 3, 2, 1]; print(numbers.lastWhere((el) => el > 2)); // 3 map(T toElement(E e)) → Iterable The current elements of this iterable modified by toElement. List<int> numbers = [1, 2, 3, 4]; print(numbers.map((el) => el + 2).toList()); // [3, 4, 5, 6] noSuchMethod(Invocation invocation) → dynamic Invoked when a nonexistent method or property is accessed. 존재하지 않는 메서드 또는 인스턴스 변수를 사용하려고 시도할 때 사용자 지정 동작을 제공하기 위해 재정의할 수 있는 특수 함수 class MyClass { @override noSuchMethod(Invocation invocation) { print('You tried to use a non-existent member: ' + '${invocation.memberName}'); } } reduce(E combine(E value, E element)) → E Reduces a collection to a single value by iteratively combining elements of the collection using the provided function. List<int> numbers = [1, 2, 3, 4]; final result = numbers.reduce((value, element) => value + element); print(result); // 10 remove(Object? value) → bool Removes the first occurrence of value from this list. List<int> numbers = [1, 2, 3, 4, 3, 2, 1]; numbers.remove(1); print(numbers); removeAt(int index) → E Removes the object at position index from this list. List<int> numbers = [1, 2, 3, 4, 3, 2, 1]; final removed = numbers.removeAt(5); print(removed); // 2 print(numbers); // [1, 2, 3, 4, 3, 1] removeLast() → E Removes and returns the last object in this list. List<int> numbers = [1, 2, 3, 4]; final removed = numbers.removeLast(); print(removed); // 4 print(numbers); // [1, 2, 3] removeRange(int start, int end) → void Removes a range of elements from the list. List<int> numbers = [1, 2, 3, 4, 5, 6, 7]; numbers.removeRange(2, 4); print(numbers); removeWhere(bool test(E element)) → void Removes all objects from this list that satisfy test. List<int> numbers = [1, 2, 3, 4, 5, 6, 7]; numbers.removeWhere((el) => el < 4); print(numbers); // [4, 5, 6, 7] replaceRange(int start, int end, Iterable replacements) → void Replaces a range of elements with the elements of replacements. List<int> numbers = [1, 2, 3, 4, 5, 6, 7]; numbers.replaceRange(3, 5, [8, 9, 10]); print(numbers); // [1, 2, 3, 8, 9, 10, 6, 7] retainWhere(bool test(E element)) → void Removes all objects from this list that fail to satisfy test. List<int> numbers = [1, 2, 3, 4, 5, 6, 7]; numbers.retainWhere((el) => el % 2 == 0); print(numbers); // [2, 4, 6] setAll(int index, Iterable iterable) → void Overwrites elements with the objects of iterable. List<int> numbers = [1, 2, 3, 4]; numbers.setAll(1, [5, 6]); print(numbers); // [1, 5, 6, 4] setRange(int start, int end, Iterable iterable, [int skipCount = 0]) → void Writes some elements of iterable into a range of this list. List<int> numbers = [1, 2, 3, 4]; numbers.setRange(1, 4, [5, 6, 7]); print(numbers); // [1, 5, 6, 7] shuffle([Random? random]) → void Shuffles the elements of this list randomly. List<int> numbers = [1, 2, 3, 4]; numbers.shuffle(); // ex. [2, 4, 3, 1] singleWhere(bool test(E element), {E orElse()?}) → E The single element that satisfies test. List<int> numbers = [1, 2, 3, 4]; // final result = numbers.singleWhere((el) => el > 2); // Error (too many elements) final result = numbers.singleWhere((el) => el < 2); // OK print(result); // 1 skip(int count) → Iterable Creates an Iterable that provides all but the first count elements. List<int> numbers = [1, 2, 3, 4]; final result = numbers.skip(2); print(result.toList()); // [3, 4] skipWhile(bool test(E value)) → Iterable Creates an Iterable that skips leading elements while test is satisfied. List<int> numbers = [1, 2, 3, 4]; final result = numbers.skipWhile((el) => el.isOdd); print(result.toList()); // [2, 3, 4] sort([int compare(E a, E b)?]) → void Sorts this list according to the order specified by the compare function. List<int> numbers = [1, 2, 3, 4]; numbers.sort((a, b) => b.compareTo(a)); print(numbers); // [4, 3, 2, 1] sublist(int start, [int? end]) → List Returns a new list containing the elements between start and end. List<int> numbers = [1, 2, 3, 4]; print(numbers.sublist(2, 4)); // [3, 4] take(int count) → Iterable Creates a lazy iterable of the count first elements of this iterable. List<int> numbers = [1, 2, 3, 4]; print(numbers.take(3).toList()); // [1, 2, 3] takeWhile(bool test(E value)) → Iterable Creates a lazy iterable of the leading elements satisfying test. List<int> numbers = [1, 2, 3, 4]; print(numbers.takeWhile((el) => el.isOdd).toList()); // [1] toList({bool growable = true}) → List Creates a List containing the elements of this Iterable. Iterable<int> numbers = [1, 2, 3, 4]; print(numbers.toList()); // [1, 2, 3, 4] toSet() → Set Creates a Set containing the same elements as this iterable. List<int> numbers = [1, 2, 3, 4, 3, 1]; print(numbers.toSet()); // {1, 2, 3, 4} toString() → String A string representation of this object. List<int> numbers = [1, 2, 3, 4, 3, 1]; print(numbers.toString()); // "[1, 2, 3, 4, 3, 1]" where(bool test(E element)) → Iterable Creates a new lazy Iterable with all elements that satisfy the predicate test. List<int> numbers = [1, 2, 3, 4, 3, 1]; print(numbers.where((el) => el.isEven).toList()); // [2, 4] whereType() → Iterable Creates a new lazy Iterable with all elements that have type T. List<Object> numbers = [1, 2, 'fat', 4, 'thin', 1]; print(numbers.whereType<int>().toList()); // [1, 2, 4, 1]
- #Dart
- #List
- #Method
[노마드코더 플러터] Dart Classes
Dart Class 기본 사용법 class Player { String name = 'moz'; // class 에서 property 를 선언할 때는 타입을 사용한다. (↔ function 에서 variable 을 사용할 때는 var ) final String nickname = 'moz'; // 변경할 수 없게 final 로 선언 int xp = 1500; void sayHello() { print("Hi my name is $name"); // this 를 사용하지 않아도 됨 var name = 'moz'; print("Hi my name is ${this.name}"); // 동일한 이름의 variable 이 method 내에 있을 경우는 this 로 참조 } } void main() { var player = Player(); // new 를 붙여도 되지만 없어도 됨 print(player.name); // moz player.name = 'tiq'; print(player.name); // tiq player.nickname = 'tiq'; // Error player.sayHello(); } Constructor (생성자) class Player { final String name; // All final variables must be initialized, but 'name' isn't. int xp; // Non-nullable instance field 'xp' must be initialized. Player(String name, int xp) { // constructor this.name = name; // 'name' can't be used as a setter because it's final. this.xp = xp; } } Error 유형 (아래 3가지 모두 해당) : Non-nullable instance field {0} must be initialized. null 이 될 수 없는 유형의 타입인 경우 초기화되지 않은 경우 late 로 처리되지 않은 경우 // late 로 처리를 하면 위에서 발생한 에러가 해결된다. class Player { late final String name; late int xp; // constructor Player(String name, int xp) { this.name = name; this.xp = xp; } } // 훨씬 간결해진 표현 class Player { // late 도 제거할 수 있다. final String name; int xp; // constructor Player(this.name, this.xp); // 중복선언된 타입을 제거할 수 있다. } Named Constructor Parameters class Player { final String name; int xp; String team; int age; // The parameter {0} can't have a value of 'null' because of its type, but the implicit default value is 'null'. // Positional Constructor 에서 Named Constructor 로 변경하면 에러가 발생하게 되는데 required 키워드를 붙임으로써 해결 Player({ required this.name, required this.xp, required this.team, required this.age, }); } void main() { // 파라미터 순서와 상관없이 값을 입력할 수 있음 var player = Player(name: 'moz', xp: 12, team: 'art', age: 13); var player2 = Player(team: 'tho', name: 'ben', age: 15, xp: 200); } Named Constructor class Player { final String name; int xp, age; String team; Player({ required this.name, required this.xp, required this.team, required this.age, }); // team, xp 에 기본값을 설정한 Named Constructor with named parameters Player.createBluePlayer({required String name, required int age}) : this.age = age, this.name = name, this.team = 'blue', this.xp = 0; // team, xp 에 기본값을 설정한 Named Constructor with positional parameters Player.createRedPlayer(String name, int age) : this.age = age, this.name = name, this.team = 'red', this.xp = 0; } void main() { var player = Player.createBluePlayer(name: 'moz', age: 13); var player2 = Player.createRedPlayer('ben', 15); } Named Constructor 를 이용한 API 연동 예시 class Player { final String name; int xp; String team; Player.fromJson(Map<String, dynamic> playerJson) : name = playerJson['name'], xp = playerJson['xp'], team = playerJson['team']; void sayHello() { print("Hi my name is $name"); } } void main() { var apiData = [ {"name": "moz", "xp": 0, "team": "tiq"}, {"name": "tho", "xp": 0, "team": "ven"}, {"name": "shu", "xp": 0, "team": "ber"} ]; apiData.forEach((playerJson) { var player = Player.fromJson(playerJson); player.sayHello(); }); } Cascade Notation class Player { String name; int xp; String team; Player({ required this.name, required this.xp, required this.team, }); void sayHello() { print("Hi my name is $name"); } } void main() { var moz = Player(name: 'moz', xp: 1000, team: 'red'); moz.name = 'tiq'; moz.xp = 2000; moz.team = 'blue'; moz.sayHello(); // Cascade Notation Player(name: 'bee', xp: 1000, team: 'red') ..name = 'tho' ..xp = 3000 ..team = 'green' ..sayHello(); } Enums // "red" 와 같이 문자열 형태로 쓰지 않아도 된다. enum Team { red, blue } enum XPLevel {beginner, medium, pro} class Player { String name; XPLevel xp; // enum XPLevel 타입으로 변경되었다. Team team; // enum Team 타입으로 변경되었다. Player({ required this.name, required this.xp, required this.team, }); void sayHello() { print("Hi my name is $name"); } } void main() { var moz = Player(name: 'moz', xp: XPLevel.beginner, team: Team.blue); moz.sayHello(); moz ..name = 'tho' ..xp = XPLevel.pro ..team = Team.red ..sayHello(); } Abstract Classes 추상화 클래스로는 객체를 생성할 수 없다. 다른 클래스들이 직접 구현해야하는 메소드들을 모아놓은 일종의 청사진 (Blueprint) 특정 메소드를 구현하도록 강제한다. abstract class Human { void walk(); } class Player extends Human { // 구현하지 않으면 에러 발생 void walk() { print('I am walking'); } } Inheritance class Human { final String name; // Human(this.name); // positional parameter Human({required this.name}); // named parameter sayHello() { print("Hi my name is $name"); } } enum Team { red, blue } class Player extends Human { final Team team; /// 부모객체에서 필요한 값을 초기화 // 부모객체의 생성자가 named parameter 인 경우 Player({required this.team, required String name}) : super(name: name); // 부모객체의 생성자가 positional parameter 인 경우 // Player({required this.team, required String name}) : super(name); // super parameter 를 쓰는 것을 권장하고 있다. // Player({required this.team, required super.name}); @override void sayHello() { super.sayHello(); print('and I play for ${team}'); } } void main() { var player = Player(team: Team.red, name: 'moz'); player.sayHello(); } Mixins Mixin : 생성자가 없는 클래스 하나의 클래스에만 사용한다면 의미가 없다. 핵심은 여러 클래스에 재사용이 가능하다는 점이다. // mixin class mixin class Strong { final double stengthLevel = 1500.99; } mixin class QuickRunner { void runQuick() { print('ruuuuuuun!'); } } mixin class Tall { final double height = 1.99; } enum Team { red, blue } class Player with Strong, QuickRunner, Tall { final Team team; Player({required this.team}); } class Horse with Strong, QuickRunner {} class Kid with QuickRunner {} void main() {}
- #노마드코더
- #플러터
- #Dart
[노마드코더 플러터] Dart Function (함수)
Function(함수) 의 정의 /** main 함수 바깥에 함수를 정의해도 상관없음 name 이라는 문자열 매개변수를 받고 문자열을 리턴 */ String sayHello(String name) { return "Hello $name, nice to meet you"; } // fat arrow syntax // 한 줄짜리 함수인 경우 다음과 같이 줄여 쓸 수 있다. String sayHello(String name) => "Hello $name nice to meet you"; num plus(num a, num b) => a + b; // void : 아무것도 리턴하지 않음 void main() { print(sayHello('moz')); } Named Parameters Positional parameter (일반적인 매개변수 사용법) // 각 매개변수가 무엇을 의미하는지 알기가 힘들다. String sayHello(String name, int age, String country) { return "Hello $name nice to meet you, you are $age years old from $country"; } void main() { print(sayHello('moz', 19, 'korea'); } Named Parameter // default value 를 설정할 수 있음 String sayHello({ String name = 'moz', int age = 12, String country = 'korea' }) { return "Hello $name nice to meet you, you are $age years old from $country"; } // default value 를 지정하고 싶지 않다면? required String sayHello({ required String name, required int age, required String country }) { return "Hello $name nice to meet you, you are $age years old from $country"; } void main() { // 매개변수 순서를 기억할 필요가 없음 print(sayHello( age: 12, country: 'korea', name: 'moz' )); } Optional Positional Parameters // Non-optional parameters can't have a default value. String sayHello(String name, int age, [String? country = 'korea']) { return "Hello $name nice to meet you, you are $age years old from $country"; } void main() { print(sayHello('moz', 12)); } QQ Operator // Case 1 String capitalizeName(String? name) { if (name != null) { return name.toUpperCase(); } return 'TIQ'; } // Case 2 String capitalizeName(String? name) => name != null ? name.toUpperCase() : 'TIQ'; // Case 3 (QQ) String capitalizeName(String? name) => name?.toUpperCase() ?? 'TIQ'; void main() { print(capitalizeName('moz')); // MOZ print(capitalizeName(null)); // TIQ } // QQ assigned operator void main() { String? name; name ??= 'moz'; // name 이 null 이면 moz 할당 print(name); // moz } Typedef // 자료형에 alias 를 붙일 수 있다. typedef ListOfInts = List<int>; typedef UserInfo = Map<String, String>; ListOfInts reverseListOfNumbers(ListOfInts list) { var reversed = list.reversed; // iterable type return reversed.toList(); } String sayHi(UserInfo userInfo) { return "Hi ${userInfo['name']}"; } void main() { print(reverseListOfNumbers([1, 2, 3])); // [3, 2, 1] print(sayHi({"name": "moz"})); // Hi moz }
- #노마드코더
- #플러터
- #Dart
[노마드코더 플러터] Dart 자료형
Dart 자료형의 특징 모든 자료형은 Object 이다. 기본 자료형 /* 문자열 */ String name = "moz"; // 큰따옴표 OK String name = 'moz'; // 작은따옴표 OK /* 불리언 */ bool alive = true; // or false /* 숫자형 */ int age = 12; double money = 69.99; // int, double 둘 다 허용이 가능한 변수는 num // int 와 double 의 부모 클래스이다. num x = 12; // int OK x = 1.1; // double OK String Interpolation 문자열에 변수를 추가하는 법 var name = 'moz'; var age = 10; var greeting = "Hello everyone, my name is $name and I'm ${age + 2}"; List /* 리스트를 사용하는 두가지 방식 */ var numbers = [1, 2, 3, 4, 5]; List<int> numbers = [1, 2, 3, 4, 5]; numbers.add(1); // OK numbers.add('moz'); // Error /* collection if */ var giveMeFive = true; var numbers = [ 1, 2, 3, 4, if (giveMeFive) 5, ]; /* collection for */ var oldFriends = ['moz', 'tiq']; var newFriends = [ 'goo', 'sik', 'jin', for (var friend in oldFriends) "$friend" ]; Maps key, value 는 어떤 자료형도 사용 가능 // 컴파일러가 타입 추론 - Map<String, Object> var player = { 'name': 'moz', 'xp': 19.99, 'superpower': false } // var 대신 명시적으로 이렇게 사용 가능 Map<String, Object> player = { 'name': 'moz', 'xp': 19.99, 'superpower': false } Sets 모든 아이템은 유니크하다. // 컴파일러가 타입 추론 - Set<int> var numbers = {1, 2, 3}; numbers.add(1); // Error 는 나지않지만 결과 : {1, 2, 3}
- #노마드코더
- #플러터
- #Dart
[노마드코더 플러터] Dart 소개와 Variables (변수)
Dart 소개 Dart 는 UI 에 최적화 되어 있다. Dart 는 두개의 컴파일러 (Dart Web, Dart Native) 를 갖고 있다. Dart Web : dart to javascript compiler Dart Native : dart to multiple architecture of different CPU (IOS, Android, Windows, Linux, Mac & IOT ..) compiler Compile 방식 AOT (ahead-of-time) 컴파일을 먼저하고, 그 결과인 바이너리를 배포 개발이 완료가 되면 사용 JIT (just-in-time) 개발중일 때만 사용 dart VM 을 사용해서 코드의 결과를 즉시 화면에 보여줌 가상머신에서 동작하는 거라 조금 느림 Null safety 를 도입 Flutter 에서 Dart 를 도입한 이유 AOT 와 JIT 둘다 제공 구글이 Flutter 와 Dart 를 만들었음 (ex. Flutter 를 위해 Dart 를 최적화할 수 있음) Dart Playground DartPad Variables (변수) Dart 는 항상 void main() 에서 시작한다. 문장은 항상 세미콜론(;) 으로 끝난다. 변수를 수정할 때는 항상 같은 타입으로만 가능하다. // 관습적으로 함수나 메소드 내부의 지역 변수를 선언할 때는 var 를 사용 // var 를 쓰든 타입을 지정하든 상관없지만 관습을 따름 var name = 'moz'; // 타입 추론 name = 1; // Error 명시적으로 타입 지정하는 것도 가능하다. // class 에서 변수나 property 를 선언할 때는 타입을 지정 // var 를 쓰든 타입을 지정하든 상관없지만 관습을 따름 String name = 'moz'; dynamic 여러가지 타입을 가질 수 있는 변수에 쓰는 키워드 (추천하지 않지만 유용한 경우가 있음) var name; // 값이 할당되지 않았으므로 name 은 dynamic 타입을 가지게 됨 name = 'moz'; // OK name = 123; // OK name = true; // OK null safety 개발자가 null 값을 참조할 수 없도록 해서 컴파일 전에 null 에러를 잡기 위함 null 이 될 수 있는 변수 지정 String name = 'moz'; name = null; // Error String? name = 'moz'; // null safety name = null; // OK name?.isNotEmpty; // name 이 null 이 아니면 isNotEmpty 수행 final 키워드 재할당하지 못하는 변수를 정의함 (const 와 다름) 런타임에 할당 가능 final name = 'moz'; name = 'tiq'; // Error final String name = 'moz'; // Type 지정도 가능 late 는 초기 데이터 없이 변수 선언을 가능하게 함 late final String name; print(name); // Error (값이 할당되지 않은 상태에서 접근할 수 없음) // API 를 호출하여 데이터를 받아온 후 아래 단계 진행 name = 'moz'; name = 123; // Error const 키워드는 컴파일 타임에 이미 할당해서 (알고 있는 값) 사용 javascript 의 const 는 Dart 의 final 과 유사 런타임에 할당 불가 const API = 'https://api.com/api'; const API = fetchApi(); // Error final API = fetchApi(); // OK
- #노마드코더
- #플러터
- #Dart