앞으로 포스팅 될 이 글은 지금 이 블로그의 개발기이며, 개발 과정중에 작성된게 아닌 작성 후의 회고록에 가깝다.
따라서 모든 개발 과정을 담은 것은 아니지만, 참고하거나 주의하면 도움이 될만 한 정보들을 작성해 나가고자 한다.
최근들어 Chat GPT와 같은 대화형 언어모델이 stackoverflow
역할을 대체하는 듯 해도, 사용해본 사람은 그 한계를 느낄거다. 특히 최신정보에 한해서는 더더욱.
글을 쓴 시점에서, 이 블로그는 최신의 버전을 사용했으므로, 관련정보가 필요한 누군가에게 도움이 된다면 좋을 것 같다.
참고로, 사용된 모듈의 버전은 다음과 같다.
- frontend
"next": "13.5.2",
"tailwindcss": "3.3.3",
- backend
"@strapi/strapi": "4.14.4",
13 버전에 들어서, Nextjs는 많은 변화가 있었다.
app폴더의 사용으로 Server component와 Client component를 구분해서 사용해야만 한다. 이는 react 18에서 생겨난 개념인데, Nextjs문서에 이에 대한 내용이 가장 먼저 나와야 하지 않나 싶을 정도로, 13버전에서는 중요한 개념이라 생각한다.
(1)의 도표에 따르면, 서버 컴포넌트에서는 useState, useEffect등 각종 hook을 비롯하여, onClick같은 이벤트리스너나 browser api(window객체)를 사용할 수 없다. 13버전의 서버 컴포넌트에서는 기존 React를 쓰던 방식에서 많은 제약이 생겨나는 것 처럼 보일 수 있다.
특히, 웹개발의 시작을 React만 사용해왔던 사람이라면, 손발이 묶여버린듯한 느낌을 받을 지도 모른다.
하지만 서버컴포넌트의 역할은 단지 getServerSideProps, getStaticProps와 같은 함수와 같은 역할을 한다. 클라이언트 컴포넌트라고 해서, 초기에 렌더링 되지 않는게 아니다.
api에서 받은 데이터를 SSR의 색인을 위해 초기 렌더링에 포함하려면, 몇가지 방법이 있다.
- 서버 컴포넌트에서 데이터 형태를 작성한다.
- react-query의 hydrate같은 기능을 이용한다.
- 서버 컴포넌트에서 가공된 값을 클라이언트 컴포넌트의 props에 던져 그대로 쓴다.
- SWR의 serialize를 사용한다.
클라이언트 컴포넌트는 파일 상단에 'use client'를 선언하면 된다. 이를 선언하고 나면 해당 컴포넌트는 기존 React작성 하듯이 하면 된다. 여기서 주의할 점은, 초기에 렌더링을 하려면 useState와 같은 상태 값을 거치면 안된다.
이를 활용하면 다음과 같은 형태가 된다
- server component예시
import ClientComponent from "./client.tsx";
export default async function ServerComponent() {
const response = await fetch("https://.../api/endpoint", {cache: 'no-cache'})
const data = await response.json()
if(!data) return <div>500 internal error.</div>
return <div>
<ClientComponent data={data} />
</div>
}
- client component예시
"use client";
import { useEffect, useState } from "react";
interface SampleProps {
data: ResponeType<DataType>;
}
export default function ClientComponent({ data }: SampleProps) {
const [state, setState] = useState<DataType | null>(null);
useEffect(() => {
setState(doSomething(data))
return () => {
setState(null)
}
}, [data]);
return <div>
<div>{data}</div> // 초기에 렌더링 된다.
<div>{state}</div> // 동적으로 렌더링 되므로, 초기에는 값이 없다.
</div>;
}
Strapi는 오픈 소스의 Headless CMS(Content Management System)이다. 워드프레스를 경험해본적 있다면, 그 관리자패널과 백엔드의 역할을 담당하는 부분이라고 보면 이해하기 쉬울것같다.
Strapi는 Node.js로 만들어져 있고, RESTful API나 GraphQL API를 쉽게 생성할 수 있어서, 다양한 프론트엔드 기술스택과 잘 연동될 수 있다. 사용자 인증, 권한 관리, 파일 업로드 등 다양한 기능도 지원하고, 커스텀 플러그인을 추가할 수 있어 확장성이 좋다. 대시보드에서 데이터 모델을 정의하거나, API 설정, 사용자 권한 등을 쉽게 설정할 수 있고, 생성된 모델을 기반으로 API가 생성되며, 필요시 컨트롤러를 추가해서 원하는 형태의 API를 개발 할 수도 있다.
복잡한 로직이 요구되는게 아니라면, 프론트엔드 프로젝트에만 집중하고 싶을때는 사용해보길 추천한다.
strapi 구성
# npm
npx create-strapi-app@latest my-project
# yarn
yarn create strapi-app my-project
명령어를 실행하면 nextjs설치할때처럼 설치 옵션을 선택할 수 있다. quickstart로 설치하면 javascript와 Sqlite데이터베이스를 기본값으로 설치하게 된다.
이 프로젝트는 Typescript / postgresql로 세팅 되어있다.
설치 후에 api서버로 호스트 URL을 다음과 같이설정해 주었다.
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: 'https://api-unsangu.obj.kr',
app: {
keys: env.array('APP_KEYS'),
},
webhooks: {
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false),
},
});
url의 값만 추가해주었고 나머지는 기본 값으로 세팅 되어있다.
만약 sqlite로 세팅해 뒀다면, 바로 yarn develop(또는 npm run develop)을 실행하면 되지만,
나는 postgresql을 사용하기로 했고, 로컬에 postgresql을 설치하고 싶지 않기 때문에, docker를 사용하기로 했다.
m1환경이라서 strapi에서 제공한 docker와는 조금 다르다.
FROM node:18-buster-slim
RUN apt-get update && apt-get install -y build-essential gcc autoconf automake zlib1g-dev libpng-dev nasm bash libvips-dev git && apt-get clean && rm -rf /var/lib/apt/lists/*
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
ENV NPM_CONFIG_PREFIX=/home/node/global
ENV PATH=$PATH:/home/node/global/bin
USER node
WORKDIR /opt/app
COPY --chown=node:node package.json yarn.lock ./
RUN yarn global add node-gyp
RUN yarn config set network-timeout 600000 -g && yarn install
ENV PATH /opt/app/node_modules/.bin:$PATH
WORKDIR /opt/app
COPY --chown=node:node . .
RUN yarn build
EXPOSE 1337
CMD ["yarn", "develop"]
version: "3"
services:
unsangu_strapi:
container_name: unsangu_strapi
build: .
image: strapi:latest
restart: unless-stopped
env_file: .env
environment:
DATABASE_CLIENT: ${DATABASE_CLIENT}
DATABASE_HOST: strapiDB
DATABASE_PORT: ${DATABASE_PORT}
DATABASE_NAME: ${DATABASE_NAME}
DATABASE_USERNAME: ${DATABASE_USERNAME}
DATABASE_PASSWORD: ${DATABASE_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
ADMIN_JWT_SECRET: ${ADMIN_JWT_SECRET}
APP_KEYS: ${APP_KEYS}
NODE_ENV: ${NODE_ENV}
volumes:
- ./config:/opt/app/config
- ./src:/opt/app/src
- ./package.json:/opt/package.json
- ./yarn.lock:/opt/yarn.lock
- ./.env:/opt/app/.env
- ./public/uploads:/opt/app/public/uploads
ports:
- "1337:1337"
networks:
- obj_network
depends_on:
- strapiDB
strapiDB:
container_name: strapiDB
platform: linux/arm64 #for platform error on Apple M1 chips
restart: unless-stopped
env_file: .env
image: postgres:12-alpine
environment:
POSTGRES_USER: ${DATABASE_USERNAME}
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
POSTGRES_DB: ${DATABASE_NAME}
volumes:
- strapi-data:/var/lib/postgresql/data/ #using a volume
#- ./data:/var/lib/postgresql/data/ # if you want to use a bind folder
ports:
- "5432:5432"
networks:
- obj_network
volumes:
strapi-data:
networks:
obj_network:
driver: bridge
external: true
환경변수는 strapi 설치할때 자동으로 생성된 환경변수 이름 그대로 사용했다.
docker-compose up --build -d
그런 다음 설정한 host url로 접속하게 되면 관리자 계정을 설정하라고 뜬다. 워드프레스를 경험해본 사람이라면 익숙한 화면일것같다. 꽤 많은 부분에서 비슷한 모습을 보여준다.
설정 후에 관리자 화면으로 들어가게 되는데, Content-Type Builder메뉴에서 Model을 설정할 수 있다.
wordpress에서는 커스텀 필드 타입과 같다고 볼 수 있는데, wordpress에서는 php에 직접 명시해서 생성해야했던것과는 달리, strapi에서는 ui상에서 커스텀 필드(모델 또는 스키마)를 설정할 수가 있다. COLLECTION TYPES와 SINGLE TYPES로 나뉘며, COMPONENTS는 필드 내에서 반복적이거나, 특별한 필드 요소들을 그룹화 할 수 있다고 보면 될것 같다.
필드를 추가하고 나면, 바로 api를 사용할 수 있도록 src/api폴더에 파일이 생성된다. koa기반이며 strapi가 함수를 확장한 형태다. 이때 생성된 controller나 routes를 수정하여, 기능을 추가하거나 덮어쓸수 있다.
api 사용
우선 collection types에 post를 만들고, 다음과 같이 필드를 추가해 주었다.
post fileds.png
그리고 settings 메뉴에 가서 Roles -> Public 편집 선택 후에 find, findOne을 체크해주었다.
- 별도로 token을 받아서 권한을 획득 하길 원하면 Settings -> api tokens에 토큰을 추가한 후에 헤더에 담아 이용하면 된다.
roles.png
이렇게 하면 host_url/api/posts 또는 host_url/api/posts/id값으로 get요청을 호출할 수 있다.
- /api/posts get 요청 결과
{
"data": [
{
"id": 10,
"attributes": {
"title": "Nextjs13 + Tailwind + Strapi4 블로그 1",
"createdAt": "2023-10-18T08:39:26.302Z",
"updatedAt": "2023-10-21T13:54:50.551Z",
"publishedAt": "2023-10-18T08:39:28.996Z",
"slug": null,
"summary": "Next.js 13 작성 변경점 및 유의사항"
}
}
],
"meta": {
"pagination": {
"page": 1,
"pageSize": 25,
"pageCount": 1,
"total": 1
}
}
}
요청을 확인하면 contents필드와 thumbnail필드가 보이지 않는데, populate쿼리를 포함해서 불러올 수가 있다.
?populate=*를 하면 관련된 전체 필드를 불러올 수 있다. 혹은 "populate[필드이름][fields][0]=필드하위이름"을 이용해서 원하는 테이블의 원하는 필드만 불러올 수도 있다.
const thumbnailQuery = `&populate[thumbnail][fields][0]=url&populate[thumbnail][fields][1]=width&populate[thumbnail][fields][2]=height&populate[thumbnail][fields][3]=hash`
strapi는 이렇게 구성하고 develop으로 실행해 두면 커스텀 테이블을 확장하면서 테스트하고, api를 하며 개발을 할 수 있게 된다.
근래들어서 꽤 알려진 tailwind를 이 블로그에 처음으로 적용해 보았다.
si 실무에 있을때 적용해볼까 고려했지만, 아무래도 Figma 디자인 시안과의 맞추려고 하다보면 tailwind의 대부분은 쓰지 않을것 같다는 생각에 접어두었었다.
우선 결론부터 말하자면, 사용해서 나쁠게 없다.
특히, Next.js13 버전을 사용한다면,
Next.js13은 그 특성상 서버 컴포넌트와 클라이언트 컴포넌트로 구분되고 있고, 이때 프론트엔드 개발자들이 많이 애용하는 styled(이하 css-in-js)의 사용은 현재(23년 10월경)까지도 클라이언트 컴포넌트에서만 사용이 가능하다. 라이브러리가 최신의 react버전으로 동시 렌더링(concurrent rendering)을 포함해야 서버 컴포넌트에서 지원 가능할것이다.
그렇기에, styled를 사용하고자하면 서버사이드에서는 css, 또는 sass 모듈, 클라이언트에서는 css-in-js로 나뉘게 되는데, 관리포인트가 증가하면, 그만큼 시간과 비용이 소모될 수 밖에 없다. 특히 공통으로 사용되는 theme에셋을 사용하는 경우에도 불리한 점을 안고 갈 수밖에 없다. but, 해결책이 아주 없는건 아니다. (나중에 링크 포함)
tailwind를 사용한다면, 서버사이드에서도 클라이언트 사이드에서도 사용이 가능하다.
font나 padding등의 값이 rem단위이긴해도, 직접 선언할 수도 있고, className="w-[100px] top-[60px]"
tailwind.config.j(t)s
에서 theme를 수정해서 쓸 수도 있다.
css는 tailwind의 Layer Directives를 통해서 tailwind cli나 post css를 통해서 전처리되는 유틸리티 지시어나, 함수를 사용할 수도 있다.
몇가지 사용하면서 느낀점으로는,
- 레이아웃을 구성할때, 그 무엇보다도 빠르고 편리하게 짜내려갈 수 있다. ide에서 tailwindCSS.emmetCompletions을 켜놓으면, 특히나 더 편할것이다.
- 복잡합 디자인 요소를 구성하다보면 className이 엄청나게 길어진다. 화면 밖으로 나가버릴 정도로 길어진다.
Prettier를 설정해도 자동으로 줄바꿈을 해주지는 않는다 관련 .
곤란하다.어느 쪽도 마음이 편하지 않다.