sqlite 설치
1. 설치 npm install better-sqlite3 2.initdb.js 추가 3.sqlite3 데이터 가져오기 app/lib 에서 meals.js 추가 ===================================================== ===================================================== xss 보안 설치 npm install slugify xss
소스 :
https://github.com/braverokmc79/macaronics-react-udemy-ex25
https://main--dapper-dango-935a92.netlify.app/
437. Share Meal 양식에 대한 기초
438. 커스텀 이미지 피커(선택 도구)입력 컴포넌트에 대한 기초
image-picker.js
'use client'; import { useRef, useState } from "react"; import classes from "./image-picker.module.css"; import Image from "next/image"; export default function ImagePicker({label, name}) { const [pickedImage, setPickedImage] = useState(); const imageInput=useRef(); //버튼 클릭시 file Input 태그 클릭처리 const handlePickClick =(e)=>{ imageInput.current.click(); }; //이미지 변경시 function handleImageChange(event){ const file=event.target.files[0]; if(!file){ setPickedImage(null); return; } //1.fileReader 설정하기 const fileReader=new FileReader(); fileReader.onload=()=>{ setPickedImage(fileReader.result); }; //2. 설정한 fileReader 에 file fileReader.readAsDataURL(file); } return ( <div className={classes.picker}> <label htmlFor="image">{label}</label> <div className={classes.controls}> <div className={classes.preview}> {!pickedImage && <p>아직 이미지거 선택되지 않았습니다.</p>} {pickedImage&& ( <Image src={pickedImage} alt="선택한 이미지" fill /> )} </div> <input //classes.input 으로 display: none; 처리 className={classes.input} type="file" id="image" accept="image/png, image/jpeg, image/gif" name={name} ref={imageInput} onChange={handleImageChange} required /> <button className={classes.button} type="button" onClick={handlePickClick} > 이미지 선택 </button> </div> </div> ) }
이 코드는 React를 사용하여 이미지 선택기 컴포넌트를 만드는 것입니다. 주요 기능과 구성 요소는 다음과 같습니다.
useState와 useRef: useState는 선택된 이미지를 관리하고, useRef는 파일 입력 요소에 대한 참조를 생성합니다.
handlePickClick 함수: 이 함수는 사용자가 ‘이미지 선택’ 버튼을 클릭하면 실행되며, 실제로는 숨겨진 파일 입력 요소를 클릭하는 것과 같습니다.
handleImageChange 함수: 이 함수는 사용자가 이미지를 선택하면 실행됩니다. 선택된 파일을 읽고, 그 결과를 pickedImage 상태에 저장합니다.
렌더링 부분: 이 부분에서는 레이블, 이미지 미리보기, 파일 입력 요소, 그리고 ‘이미지 선택’ 버튼을 렌더링합니다.
이미지가 선택되지 않았을 경우, "아직 이미지가 선택되지 않았습니다."라는 메시지를 표시하고, 이미지가 선택되면 해당 이미지를 렌더링합니다.
이 컴포넌트는 사용자가 이미지를 선택하고, 그 이미지를 미리 볼 수 있게 해줍니다. 이는 프로필 사진 업로드 등의 기능에서 유용하게 사용될 수 있습니다.
이 컴포넌트는 label과 name 두 개의 props를 받아서, 각각 레이블과 파일 입력 요소의 이름으로 사용합니다. 이를 통해 여러 곳에서 재사용 가능하며,
각각 다른 레이블과 이름을 가질 수 있습니다.
439. 피커에 이미지 미리보기 추가
export default function ImagePicker({label, name}) { const [pickedImage, setPickedImage] = useState(); //이미지 변경시 function handleImageChange(event){ const file=event.target.files[0]; if(!file){ setPickedImage(null); return; } //1.fileReader 설정하기 const fileReader=new FileReader(); //자바스크립트 FileReader 객체가 파일 읽기 작업을 성공적으로 완료 했을때 발생 fileReader.onload=()=>{ setPickedImage(fileReader.result); }; / fileReader.readAsDataURL(file); }
fileReader.readAsDataURL(file);
file은 사용자가 선택한 이미지 파일입니다.
이 파일을 readAsDataURL 메서드에 전달하면, FileReader 객체는 이 파일을 읽고 그 결과를 데이터 URL로 변환합니다.
이 데이터 URL은 이미지 데이터를 문자열 형태로 나타내며, 이 문자열은 웹 페이지에서 직접 사용할 수 있습니다. 예를 들어,
이 데이터 URL을 img 태그의 src 속성에 설정하면 해당 이미지를 웹 페이지에 표시할 수 있습니다.
따라서 이 코드는 사용자가 선택한 이미지 파일을 읽고, 그 결과를 데이터 URL로 변환하여, 이후에 이미지를 렌더링하는 데 사용하게 됩니다.
이렇게 하면 사용자가 선택한 이미지를 실시간으로 미리 볼 수 있습니다
440.이미지 피커 컴포넌트 개선하기
‘이미지 피커’ 컴포넌트에 대해 개선할 수 있는 또는 해야 하는 두 가지 사항이 있습니다:
1) 이미지가 선택되지 않은 경우 미리보기된 이미지를 재설정:
if (!file) 블록에 setPickedImage(null); 추가:
if (!file) { setPickedImage(null); return; }
2) (숨겨진) <input> 요소에 필수 prop을 추가:
<input className={classes.input} type="file" id={name} accept="image/png, image/jpeg" name={name} ref={imageInput} onChange={handleImageChange} required />
이렇게 하면 이미지가 선택되지 않을 경우 <form>을 제출할 수 없도록 합니다.
441.양식 제출 처리를 위한 서버 액션 소개 및 사용법
'use server' => 이것은 server Action 이라는 것을 생성하는 서버에서, 오직 서버에서만 실행될 수 있게 보장해 주는 기능입니다.
async 를 붙여 줘야 하며, form 에서 action 의 속성값을 설정해 줄 수 있다.
form 에서 action 값은 보통 요청이 보내질 곳에 대한 path(경로) action="/some-path" 와 같이 설정되어진다. 그러나 nextjs 의 App Router 방식에서는
shareMeal() 함수를 넣어 form 의 제출을 제어 할수 있게 한다.
그리고 form 의 action 에서 설정한 함수인 shareMeal() 는 기본적으로 formData 를 받을수 있다.
function shareMeal(formData){ 'use server'; const meal={ title:formData.get('title'), summary:formData.get('summary'), instructions:formData.get('instructions'), image:formData.get('image'), creator:formData.get('name'), creator_email:formData.get('email'), slug:formData.get('slug'), } console.log(meal); }
여기서 이미지의 name 속성값은 다음과 같이 처리해 줄 수 있다.
<ImagePicker label="Your image" name="image" />
{ title: '제목', summary: '한줄요약', instructions: '요리방법', image: File { size: 104366, type: 'image/jpeg', name: 'schnitzel.jpg', lastModified: 1715850722009 }, creator: 'dfd', creator_email: 'test1@gmail.com', slug: null }
app/meals/share/page.jsx
import ImagePicker from '@/components/meals/image-picker'; import classes from './page.module.css'; export default function ShareMealPage() { async function shareMeal(formData){ 'use server'; const meal={ title:formData.get('title'), summary:formData.get('summary'), instructions:formData.get('instructions'), image:formData.get('image'), creator:formData.get('name'), creator_email:formData.get('email'), slug:formData.get('slug'), } console.log(meal); } return ( <> <header className={classes.header}> <h1> 여러분의 <span className={classes.highlight}>좋아하는 요리</span> 를 공유하세요. </h1> <p>또는 공유하고 싶은 다른 요리도 괜찮습니다.</p> </header> <main className={classes.main}> <form className={classes.form} action={shareMeal}> <div className={classes.row}> <p> <label htmlFor="name">이름</label> <input type="text" id="name" name="name" required /> </p> <p> <label htmlFor="email">이메일</label> <input type="email" id="email" name="email" required /> </p> </div> <p> <label htmlFor="title">제목</label> <input type="text" id="title" name="title" required /> </p> <p> <label htmlFor="summary">한줄요약</label> <input type="text" id="summary" name="summary" required /> </p> <p> <label htmlFor="instructions">요리방법</label> <textarea id="instructions" name="instructions" rows="10" required ></textarea> </p> <ImagePicker label="Your image" name="image" /> <p className={classes.actions}> <button type="submit">요리 공유</button> </p> </form> </main> </> ); }
442.개별 파일에 서버 액션 저장
app/meals/share/page.jsx 파일에 'use client' 적용하면 에러가 발생한다 따라서,
app/lib/actions.js 파일을 만들어 분리해 줄수 있다.
'use server' export async function shareMeal(formData){ const meal={ title:formData.get('title'), summary:formData.get('summary'), instructions:formData.get('instructions'), image:formData.get('image'), creator:formData.get('name'), creator_email:formData.get('email'), slug:formData.get('slug'), } console.log(meal); }
app/meals/share/page.jsx
'use client' import ImagePicker from '@/components/meals/image-picker'; import classes from './page.module.css'; import { shareMeal } from '@/app/lib/actions'; export default function ShareMealPage() { ~
443.XSS 보호를 위한 슬러그 생성 및 유저 입력 무결 처리하기
nextjs 에서 sqlite3 이용해서 개발시 xss 보홀를 위해 다음 라이브러리 설치를 해준다.
$ npm install slugify xss
app/lib/meals.js
export async function saveMeal(meal){ meal.slug=slugify(meal.title, {lower:true}); meal.instructions=xss(meal.instructions); }
slugify: slugify는 문자열을 URL 친화적인 형태로 변환하는 라이브러리입니다.
이 라이브러리는 문자열을 받아서 소문자로 변환하고, 공백을 하이픈(-)으로 대체하며, 특수 문자를 제거합니다.
이렇게 변환된 문자열은 URL의 일부로 사용될 수 있습니다. 예를 들어, "Hello World!"라는 문자열은 "hello-world"로 변환됩니다.
주어진 코드에서는 meal.title을 slugify하여 meal.slug에 저장하고 있습니다.
xss: xss는 Cross-Site Scripting (XSS) 공격을 방지하기 위한 라이브러리입니다.
XSS 공격은 웹사이트에 악성 스크립트를 삽입하여, 사용자의 데이터를 탈취하거나
사용자가 웹사이트와 상호작용하는 것을 조작하는 공격입니다.
xss 라이브러리는 입력된 문자열에서 악성 스크립트를 제거하여 이러한 공격을 방지합니다.
주어진 코드에서는 meal.instructions을 xss를 통해 필터링하여 저장하고 있습니다.
444.업로드 된 이미지 저장 및 데이터베이스에 저장
app/lib/meals.js
// 식사 정보를 저장하는 비동기 함수를 정의합니다. export async function saveMeal(meal) { meal.slug = slugify(meal.title, {lower: true}); // slugify 라이브러리를 사용하여 식사의 제목을 소문자 형태로 변환합니다. meal.instructions = xss(meal.instructions); // xss 라이브러리를 사용하여 식사의 지시사항에서 XSS 공격을 방지합니다. const extension = meal.image.name.split('.').pop(); // 이미지 파일의 확장자를 가져옵니다. const fileName = `${meal.slug}.${extension}`; // 파일 이름을 slug와 확장자를 이용하여 생성합니다. const stream = fs.createWriteStream(`public/images/${fileName}`); // public/images 디렉토리에 쓰기 스트림을 생성합니다. const bufferedImage = await meal.image.arrayBuffer(); // 이미지를 ArrayBuffer로 변환합니다. // 스트림에 이미지 데이터를 씁니다. 에러가 발생하면 예외를 던집니다. stream.write(Buffer.from(bufferedImage), (error) => { if (error) { throw new Error('이미지 저장에 실패하였습니다.!'); } }); meal.image = `/images/${fileName}`; // 식사 정보의 이미지 경로를 업데이트합니다. // SQLite3 데이터베이스에 식사 정보를 저장합니다. db.prepare(` INSERT INTO meals (title, summary, instructions, creator, creator_email, image, slug) VALUES ( @title, @summary, @instructions, @creator, @creator_email, @image, @slug ) `).run(meal); }
app/lib/actions.js
'use server' import { redirect } from "next/navigation"; import { saveMeal } from "./meals"; export async function shareMeal(formData){ const meal={ title:formData.get('title'), summary:formData.get('summary'), instructions:formData.get('instructions'), image:formData.get('image'), creator:formData.get('name'), creator_email:formData.get('email'), slug:formData.get('slug') } await saveMeal(meal); redirect("/meals"); }
445.useFormStatus 를 이용한 양식 제출 상태 관리
app/lib/meals-form-submit.js
"use client"; import { useFormStatus } from "react-dom"; export default function MealsFormSubmit(options) { const {pending} =useFormStatus(options.status); return ( <button type="submit" disabled={pending}> {pending ? '전송중...' : '요리 공유'} </button> ); }
useFormStatus 은 form 안에서 만 작동을 한다.
useFormStatus는 아래와 같은 프로퍼티를 가지는 status 객체를 반환합니다.
pending: 불리언 값입니다. true라면 상위 <form>이 아직 제출 중이라는 것을 의미합니다. 그렇지 않으면 false입니다.
data: FormData 인터페이스를 구현한 객체로, 상위 <form>이 제출하는 데이터를 포함합니다. 활성화된 제출이 없거나 상위에 <form>이 없는 경우에는 null입니다.
method: 'get' 또는 'post' 중 하나의 문자열 값입니다. 이 프로퍼티는 상위 <form>이 GET 또는 POST HTTP 메서드를 사용하여 제출되는지를 나타냅니다.
action: 상위 <form>의 action prop에 전달한 함수의 레퍼런스입니다. 상위 <form>이 없는 경우에는 이 프로퍼티는 null입니다.
useFormStatus 훅은 <form> 내부에 렌더링한 컴포넌트에서 호출해야 합니다.
이 훅은 폼이 현재 제출하고 있는 상태인지를 의미하는 pending 프로퍼티와 같은 상태 정보를 반환합니다.
위의 예시에서 Submit 컴포넌트는 폼이 제출 중일 때 <button>을 누를 수 없도록 하기 위해 이 정보를 활용합니다
446.서버 사이드 에서 폼 입력값 유효성 체크 방법
app/lib/action.js
'use server' import { redirect } from "next/navigation"; import { saveMeal } from "./meals"; function isInvalidText(text){ return !text || text.trim() ===''; } export async function shareMeal(formData){ const meal={ title:formData.get('title'), summary:formData.get('summary'), instructions:formData.get('instructions'), image:formData.get('image'), creator:formData.get('name'), creator_email:formData.get('email') // slug:formData.get('slug') } if(isInvalidText(meal.title) || isInvalidText(meal.summary) || isInvalidText(meal.instructions) || isInvalidText(meal.creator) || isInvalidText(meal.creator_email) || !meal.creator_email.includes('@') || !meal.image || meal.image.size===0 ){ throw new Error("입력값이 유효하지 않습니다."); } await saveMeal(meal); redirect("/meals"); }
위와 같이 유효성 체크를 실해하면 입력값 오류시 다음과 같이 에러 페이지로 이동되는 것을 볼 수 있다.
447.서버 행동 응답 및 useFormState 작업
useFormState는 React의 실험적인 훅으로, 폼 액션의 결과를 기반으로 상태를 업데이트할 수 있게 해줍니다1. 이 훅은 아래와 같이 사용됩니다.
import { useFormState } from "react-dom"; function MyComponent() { const [state, formAction] = useFormState(action, initialState); // ... }
여기서 action은 폼이 제출되거나 버튼이 눌렸을 때 호출될 함수이며,
initialState는 초기 상태를 설정하고자 하는 값입니다.
useFormState는 현재 상태와 함께 폼에서 사용하는 새로운 액션을 반환합니다.
따라서, 주어진 코드에서 useFormState(serverAction, initialState)는 serverAction이라는 폼 액션과 initialState라는 초기 상태를 사용하여 폼 상태를 관리합니다.
formState는 현재 폼 상태를 나타내고, formDispatch는 폼 액션을 나타냅니다. 이 폼 액션은 폼 제출 시 호출되며, 그 결과로 반환된 값은 새로운 폼 상태가 됩니다
1) useFormState 초기 설정
app/meals/share/page.jsx
'use client'; import ImagePicker from '@/components/meals/image-picker'; import classes from './page.module.css'; import { shareMeal } from '@/app/lib/actions'; import MealsFormSubmit from '@/components/meals/meals-form-submit'; import { useFormState } from "react-dom"; export default function ShareMealPage() { //message를 받기 때문에 초기 값 설정 const [state, formAction]=useFormState(shareMeal, {message:null});
2) action 에 기존에 shareMeal 값 대신에 action={formAction} 으로 변경
app/meals/share/page.jsx
~ <form className={classes.form} action={formAction}> ~
3) shareMeal 함수에서 prevState 파라미터 추가 , 여기서는 사용되지 않으나 반드시 추가해야 한다.
app/lib/meals.js
//form 에서 action 으로 넘겨줄때에는 기본적으로 formData 하나의 파라미터만 받아 처리되는데, //useFormStatus 로 유효성 체크시 shareMeal는 prevState 추가 해야 한다. export async function shareMeal(prevState, formData){ const meal={ title:formData.get('title'), summary:formData.get('summary'), instructions:formData.get('instructions'), image:formData.get('image'), creator:formData.get('name'), creator_email:formData.get('email') // slug:formData.get('slug') } ~
4) state.message 로 에러 표
app/meals/share/page.jsx
~ <ImagePicker label="Your image" name="image" /> {state.message && <p>{state.message}</p>} <p className={classes.actions}> <MealsFormSubmit /> </p> ~
다음과 같이 유효성 검사에서 필터링 되어 오류 메시지가 표시되는 것을 볼 수 있다
448.NextJS 캐싱 구축 및 이해
Nextjs 는 클라이언에 빠른 데이터를 보여주기 위해 공격적인 캐싱처리를 한다.
그러나 이러한 캐싱처리는 등록한 게시글이 안보이는 문제점이 발생한다.
개발 환경에서는 정상적으로 작동이 되나, 프로젝트 를 빌드후 실행하는 운영환경에서는 등록한 게시글이 보이지 않는다.
npm run build 하면 페이지들이
프로젝트가 빌드되어짐과 동시에 모든 페이지들이 렌더링 되어 페이지들이 생성 되어진다.
이렇게 미리 만들어진 페이지들은 당연히 동적인 페이지가 안된다.
449.캐시 유효성 재확인 트리거
등록완료 후 다음과 같이 revalidatePath 함수를 사용하면된다.
app/lib/actions.js
'use server' import { redirect } from "next/navigation"; import { saveMeal } from "./meals"; import { revalidatePath } from "next/cache"; function isInvalidText(text){ return !text || text.trim() ===''; } //form 에서 action 으로 넘겨줄때에는 기본적으로 formData 하나의 파라미터만 받아 처리되는데, //useFormStatus 로 유효성 체크시 shareMeal는 prevState 추가 해야 한다. export async function shareMeal(prevState, formData){ const meal={ title:formData.get('title'), summary:formData.get('summary'), instructions:formData.get('instructions'), image:formData.get('image'), creator:formData.get('name'), creator_email:formData.get('email') // slug:formData.get('slug') } if(isInvalidText(meal.title) || isInvalidText(meal.summary) || isInvalidText(meal.instructions) || isInvalidText(meal.creator) || isInvalidText(meal.creator_email) || !meal.creator_email.includes('@') || !meal.image || meal.image.size===0 ){ //throw new Error("입력값이 유효하지 않습니다."); //다음과 같이 리턴값을 반환시킨다. return { message:"입력값이 유효하지 않습니다." } } await saveMeal(meal); revalidatePath("/meals"); redirect("/meals"); }
revalidatePath는 Next.js에서 제공하는 함수로, 특정 경로의 정적 생성된 페이지를 다시 검증하도록 요청합니다.
이 함수는 서버 측에서 호출되며, 주로 데이터가 변경되었을 때 해당 변경 사항을 반영하기 위해 사용됩니다.
revalidatePath 함수는 다음과 같이 사용됩니다.
import { revalidatePath } from "next/cache"; // ... revalidatePath("/your-path");
이 함수를 호출하면, Next.js는 지정된 경로(“/your-path”)의 페이지를 다시 검증하도록 요청합니다.
이 요청이 처리되면, 해당 페이지는 다음 요청 시 새로운 데이터로 재생성됩니다.
따라서, 주어진 코드에서 revalidatePath("/meals")는 “/meals” 경로의 페이지를 다시 검증하도록 요청합니다.
이는 식사 정보가 저장된 후에 해당 변경 사항을 반영하기 위해 사용됩니다. 이렇게 하면, 사용자는 최신의 식사 정보를 볼 수 있게 됩니다.
서버 컴포넌트는 클라이언트 컴포넌트에서 사용하는 useState를 사용할 수 없어 변경된 부분이 있더라도 다시 렌더링이 되지않기 때문에
revalidatePath()를 사용하여 해당 페이지를 재검증해줍니다.
revalidatePath(path: string, type?: 'page' | 'layout'): void;
revalidatePath는 첫번째 파라미터로 page path를 받고, 두번째 파라미터로 type을 받습니다. 두번째 파라미터는 선택사항으로써, 첫번째 파라미터인 path만 입력하더라도 동작합니다.
하지만, 첫번째 파라미터만 입력시 해당 path만 재검증하고 중첩된 path들은 검증하지 않습니다.
revalidatePath('/meals');
meals 페이지만 재검증합니다.
두번째 파라미터인 타입을 입력하지 않으면 기본값으로 'page'가 적용됩니다. 'page'는 해당 페이지만 재검증하겠다는 것입니다.
다른 타입인 'layout'은 Next.js에서 사용하는 layout.js가 page.js들을 감싸고 있는 것처럼 해당 path와 관련된 모든 path들, 즉 동적인 path와 중첩 path들까지 모두 재검증한다는 뜻입니다.
revalidatePath('/meals', 'layout');
meals 페이지와 meals 페이지와 관련된 모든 페이지를 재검증합니다. (예: meals/share/, meals/[mealSlug])
전체 페이지를 재검증하려면 path에 루트 경로를 넣고, type을 레이아웃으로 선언합니다.
revalidatePath('/', 'layout');
예를들어 Share Meal 페이지라는 페이지는 form으로 이루어진 페이지입니다.
https://ihateindex.tistory.com/19
450.로컬 Filesystem 에 파일 저장 금지!
Next.js에서 캐싱은 페이지를 빠르게 로드하기 위해 사용됩니다. 그러나 이 캐싱 시스템은 이미지가 보이지 않는 문제를 일으킬 수 있습니다.
개발 환경에서는 ‘public’ 폴더에 접근이 가능하지만, 배포 환경에서는 ‘.next’ 폴더가 사용됩니다. 이 ‘.next’ 폴더는 캐싱된 페이지 등을 포함하고 있습니다.
새로운 이미지를 ‘public’ 폴더에 저장하면, 배포 환경 서버는 이 폴더를 무시합니다. 따라서, 이 이미지는 보이지 않게 됩니다.
이 문제를 해결하기 위한 한 가지 방법은 AWS S3와 같은 파일 저장 서비스를 사용하는 것입니다. 이 서비스를 사용하면, 런타임에 생성된 모든 파일을 안전하게 저장하고 이용할 수 있습니다.
그러나 이 방법은 본 강의의 범위를 벗어나므로, 이 문제는 무시하도록 합니다. 다음 강의에서는 AWS S3의 사용법에 대해 자세히 알아볼 수 있습니다.
451보너스: 업로드한 이미지를 클라우드에 저장하기(AWS S3)
이전 강의에서 설명했듯이 업로드된 파일(또는 런타임에 생성된 기타 파일)을 로컬 파일 시스템에 저장하는 것은 이상적이지 않습니다. 실행 중인 NextJS 애플리케이션에는 해당 파일을 사용할 수 없기 때문입니다.
대신AWS S3과 같은 클라우드 파일 저장소를 통해 이러한 파일(예: 업로드된 이미지)을 저장하는 것이 좋습니다.
AWS S3은(환경설정에 따라) 파일을 저장하고 제공할 수 있는 AWS 서비스입니다. 이 서비스는 무료로 시작할 수 있지만 예기치 않는 상황을 대비해서 가격페이지를 확인하는 것이 좋습니다.
이번 강의에는 AWS S3을 사용하여 업로드된 사용자 이미지를 저장하고 NextJS 웹사이트에 이미지를 제공할 수 있는 방법에 대해 설명하겠습니다.
1) AWS 계정 만들기
AWS S3을 사용하기 위해 AWS 계정이 필요합니다. 여기에서 계정을 생성할 수 있습니다.
2) S3 버킷 생성
계정을 만들고 로그인한 후 S3 콘솔로 이동하여 이른바 ‘버킷(bucket)’을 생성해야 합니다.
‘버킷’은 파일을 저장할 수 있는 용기입니다 (주석: 이미지를 포함한 모든 파일을 저장할 수 있습니다).
모든 버킷은 전 세계적으로 고유한 이름이어야 하므로 창의적인 이름이 지어야 합니다. 예를 들어 <your-name>-nextjs-demo-users-image와 같은 이름을 사용할 수 있습니다.
저는 maxschwarzmueller-nextjs-demo-users-image를 예시로 사용할 것입니다.
버킷을 만들 때 모든 기본 설정을 확인할 수 있으며 이름만 여러분이 설정해야 합니다.
3) 더미 이미지 파일 업로드
버킷이 생성되었으니 파일을 추가할 수 있습니다. => 이전에 public/images 폴더에 로컬로 저장한 더미 이미지.
그러기 위해, 생성한 버킷을 선택하고 ‘업로드(Upload)’ 버튼을 클릭합니다. 그런 다음, 이미지들을 상자에 끌어 놓고 업로드를 확인합니다.
그런 다음, 모든 이미지가 버킷에 포함됩니다:
4) 이미지를 제공할 버킷 환경설정
더미 이미지를 업로드했으니 NextJS 웹사이트에서 로딩될 수 있도록 버킷 환경설정을 해야 합니다.
기본 설정으로 인해 불가능하기 때문입니다! S3의 기본 설정은 ‘잠겨져(locked down)’있어서 그 안의 파일들이 보호되고 다른 사람이 접근할 수 없습니다.
그러나 우리의 목적을 위해 모든 사람이 이미지를 볼 수 있도록 버킷 설정을 업데이트해야 합니다.
이를 수행하려면 첫 번째 단계로 ‘권한(Permission)’ 탭을 클릭하고 ‘공용 액세스 차단(Block public access)’을 ‘편집(Edit)’합니다:
그런 다음 ‘모든 공용 액세스 차단(Block all public access)’ 체크박스와 이와 동반된 모든 체크박스를 비활성화하고 ‘변경사항 저장(Save Changes)’을 선택합니다.
확인 오버레이가 나타나면 ‘confirm’을 입력합니다.
그게 전부가 아닙니다. 다음이자 마지막 단계에 이른바 ‘버킷 정책(Bucket Policy)’을 추가해야 합니다. 이는 AWS 전용 정책 문서로 버킷에 저장된 객체의 권한을 관리할 수 있습니다.
‘권한’ 탭에 있는 ‘모든 공용 액세스 차단’ 영역 바로 아래에 이와 같은 ‘버킷 정책’을 추가할 수 있습니다:
‘편집’을 클릭한 후 다음 버킷 정책을 상자에 삽입합니다:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicRead", "Effect": "Allow", "Principal": "*", "Action": [ "s3:GetObject", "s3:GetObjectVersion" ], "Resource": [ "arn:aws:s3:::DOC-EXAMPLE-BUCKET/*" ] } ] }
DOC-EXAMPLE-BUCKET 을 여러분의 버킷 이름(저의 경우에는maxschwarzmueller-nextjs-demo-users-image)으로 변경합니다. 그리고 ‘변경사항 저장’을 클릭합니다.
이제 버킷은 해당 객체 중 하나를 가리키는 URL을 가진 모든 사용자에게 그 안에 있는 모든 객체에 대한 액세스 권한을 부여하도록 설정됩니다.
때문에 이제 세상과 공유하고 싶지 않은 어떤 파일도 버킷에 추가하면 안 됩니다!
모두 작동하는지 테스트하려면 버킷에 업로드한 이미지 중 하나를 클릭합니다.
그리고 ‘Object URL’을 클릭합니다. 열린다면 즉, 이미지를 볼 수 있다면, 모두 정상적으로 설정된 것입니다.
5) S3 이미지를 사용하기 위한 NextJS 코드 업데이트
이제 S3을 통해 이미지가 저장+제공되었으니 NextJS 앱에서 이미지를 로딩할 차례입니다.
첫 번째 단계로 public/images 폴더를 삭제하여 public/이라는 빈 폴더만 남도록 합니다.
이제 NextJS 프로젝트에 있는 .next 폴더를 삭제하고 localhost:3000/meals을 방문하면 이미지가 없는 식사들이 보입니다.
이미지를 다시 불러오기 위해 첫 단계로 initdb.js 파일을 업데이트하여 데이터베이스를 편집합니다:
이미지의 속성 값을 image: '/images/burger.jpg'에서 image: 'burger.jpg'와 같은 형식으로 모든 식사를 변경합니다.
대안으로 업데이트된 initdb.js파일이 첨부되어 있습니다.
그 다음 MealItem 컴포넌트가 있는 components/meals/meal-item.js 파일에 들어가 <Image> src를 업데이트합니다:
<Image src={`https://maxschwarzmueller-nextjs-demo-users-image.s3.amazonaws.com/${image}`} alt={title} fill />
물론 여러분의 S3 URL/버킷명을 사용해야 합니다!
새로운 src값은 버킷 객체로 연결되는 S3 URL (즉, 테스트 목적으로 클릭한 URL으로, 이미지 파일명이 끝에 없어야 합니다)을 포함한 문자열(string)입니다. 실제 이미지 이름은 ${image}을 통해 동적으로 삽입됩니다.
참고: 이 작업은 S3 버킷에 저장된 이미지의 이름이 initdb.js 파일에 참조된 경우에만 수행됩니다!
또한 app/meals/[mealSlug]/page.js 파일을 업데이트하고 이 페이지의 이미지도 S3에서 가져오는지 확인해야 합니다:
<Image src={`https://maxschwarzmueller-nextjs-demo-users-image.s3.amazonaws.com/${meal.image}`} alt={meal.title} fill />
물론 여러분의 S3 URL/버킷명을 사용해야 합니다!
이제 데이터베이스 데이터를 재설정하려면 meals.db 파일을 삭제하고(즉, SQLite 데이터베이스 파일 삭제) node initdb.js를 다시 실행하여 업데이트된 이미지 값으로 초기화합니다.
그 다음 개발 서버(npm run dev)를 다시 시작하면 /meals 페이지를 방문할 때 오류가 발생하는 것을 볼 수 있습니다:
Error: Invalid src prop (https://maxschwarzmueller-nextjs-demo-users-image.s3.amazonaws.com/burger.jpg) on `next/image`, hostname "maxschwarzmueller-nextjs-demo-users-image.s3.amazonaws.com" is not configured under images in your `next.config.js`
6) 이미지 소스로 S3 허용
기본값으로 NextJS는<Image> 컴포넌트를 사용할 때 외부 URL을 허용하지 않기 때문에 오류가 발생하는 것입니다.
이와 같은 오류를 제거하려면 이러한 URL을 명시적으로 허용해야 합니다.
이는next.config.js file을 편집하여 해결할 수 있습니다:
const nextConfig = { images: { remotePatterns: [ { protocol: 'https', hostname: 'maxschwarzmueller-nextjs-demo-users-image.s3.amazonaws.com', port: '', pathname: '/**', }, ], }, };
물론 여러분의 S3 URL/버킷명을 사용해야 합니다!
이 remotePatternsconfig을 통해 특정 S3 URL을 이미지의 유효한 소스로 사용 가능합니다.
이 config 파일을 업데이트+저장하면 /meals를 방문하여 이미지들을 다시 볼 수 있게 됩니다.
7) 업로드된 이미지를 S3에 저장
이제 더미 이미지를 다시 볼 수 있게 되었으니 유저가 생성한(즉, 업로드한) 이미지를 S3에 ‘포워딩(forward)’할 시간입니다.
이는 AWS에서 제공하는 패키지인 @aws-sdk/client-s3 package를 통해 가능합니다. 이 패키지는 S3과 상호 작용할 수 있는 기능을 제공합니다(예: 특정 버킷에 파일 저장).
npm install @aws-sdk/client-s3을 통해 해당 패키지를 설치합니다.
그 다음 lib/meals.js 파일로 이동하여 AWS S3 SDK(파일 상단에 위치)를 가져옵니다:
import { S3 } from '@aws-sdk/client-s3';
그 다음 이 줄을 추가하여 초기화합니다(예: db 객체가 생성된 줄 바로 위):
const s3 = new S3({ region: 'us-east-1' }); const db = sql('meals.db'); // <- this was already there!
이제 거의 다 됐습니다!
이제 saveMeal() 함수를 편집하고 로컬 파일 시스템에 이미지 저장과 관련된 모든 코드를 제거합니다.
대신 다음 코드를 추가합니다:
s3.putObject({ Bucket: 'maxschwarzmueller-nextjs-demo-users-image', Key: fileName, Body: Buffer.from(bufferedImage), ContentType: meal.image.type, });
물론 여러분의 S3 URL/버킷명을 사용해야 합니다!
또한 meal.image 하에 이미지 파일 이름을 저장해야 합니다:
meal.image = fileName;
마지막 saveMeal() 함수는 다음과 같습니다:
export async function saveMeal(meal) { meal.slug = slugify(meal.title, { lower: true }); meal.instructions = xss(meal.instructions); const extension = meal.image.name.split('.').pop(); const fileName = `${meal.slug}.${extension}`; const bufferedImage = await meal.image.arrayBuffer(); s3.putObject({ Bucket: 'maxschwarzmueller-nextjs-demo-users-image', Key: fileName, Body: Buffer.from(bufferedImage), ContentType: meal.image.type, }); meal.image = fileName; db.prepare( ` INSERT INTO meals (title, summary, instructions, creator, creator_email, image, slug) VALUES ( @title, @summary, @instructions, @creator, @creator_email, @image, @slug ) ` ).run(meal); }
8) NextJS 백엔드 AWS 접근 권한 부여
이제 마지막이지만 아주 중요한 단계인 NextJS 앱 S3 접근 권환 부여입니다.
버킷의 내용을 모두에게 제공하도록 S3을 설정했습니다.
하지만 모든 사람이 버킷에 작성하거나 버킷 내용을 변경할 수 있도록 설정하지 않았습니다. 또 설정해서도 안 됩니다!
그러나 현재의 S3 AWS SDK를 통한 NextJS 앱은 이것을 하려고 합니다!
앱에 적절한 권한을 부여하려면 앱에 대한 AWS 접근 키를 설정해야 합니다.
이는 루트(root) NextJS 프로젝트에 .env.local 파일을 추가함으로써 할 수 있습니다. 이 파일은 NextJS에 의해 자동으로 읽히고 거기에
설정된 환경 변수를 여러분의 앱의 백엔드(!) 부분에서 사용할 수 있게 됩니다.
다음에서 NextJS 앱 환경 변수 설정에 대한 자세한 내용을 확인할 수 있습니다: https://nextjs.org/docs/app/building-your-application/configuring/environment-variables
이 .env.local파일에 두개의 키-값 쌍(key-value pairs)을 추가해야 합니다:
AWS_ACCESS_KEY_ID=<your aws access key> AWS_SECRET_ACCESS_KEY=<your aws secret access key>
해당 접근 코드는 브라우저에 있는 AWS 콘솔 내부에서 얻을 수 있습니다. AWS 콘솔 오른쪽 상단에 있는 여러분의 계정 이름을 클릭하고 ‘보안 자격 증명(Security Credentials)’을 클릭하여 부여됩니다.
‘접근 키(Access Key)’ 부분으로 스크롤하여 새 접근 키를 생성합니다. 값을 복사하여 .env.local 파일에 붙여 넣고 이 키는 다른 사람과 절대 공유하면 안됩니다! Git의 다른 사용자에게 공유하는 등의 행동을 하면 안 됩니다.
더 자세한 내용은 다음에서 볼 수 있습니다:
https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html
이 모든 것을 마치고 새로운 식사를 생성하고 이미지를 업로드하면 /meals에서 드디어 볼 수 있게 됩니다. 심지어 제작 중에도 가능합니다! 이 이미지들이 S3에 저장되었기 때문입니다!
완성되고 조정된 코드가 첨부되어 있습니다. 그러나 .env.local 파일은 포함되어 있지 않다는 점 참고하시기 바랍니다. 첨부된 코드를 실행하려면 각자의 자격 증명을 사용해 여러분이 직접 추가해야 합니다.
452.정적 메타데이터 추가
nextjs 공식문서 참조:
https://nextjs.org/docs/app/building-your-application/optimizing/metadata
app/layout.js
import MainHeader from '@/components/main-header/main-header'; import './globals.css'; export const metadata = { title: '넥스트 푸드', description: '맛있는 식사, 음식을 사랑하는 커뮤니티와 공유하는 것.', }; export default function RootLayout({ children }) { return ( <html lang="ko"> <body> <MainHeader /> {children} </body> </html> ); }
1)페이지 메타데이터: Next.js에서는 'metadata’라는 이름의 export된 변수나 상수를 모든 페이지 및 레이아웃 파일에서 찾아냅니다.
이 ‘metadata’ 객체에서는 페이지의 메타데이터 필드를 지정할 수 있습니다.
2)메타데이터 적용 범위: 메타데이터를 레이아웃에 추가하면, 그 레이아웃이 감싸고 있는 모든 페이지에 자동으로 적용됩니다.
3)페이지에 메타데이터가 존재하면, 페이지 메타데이터가 우선 적용됩니다. 중첩된 레이아웃에 메타데이터가 존재하면, 레이아웃 메타데이터가 우선 적용됩니다.
4)메타데이터 추가: ‘All Meals’ 페이지에 메타데이터를 추가하는 예시가 있습니다. 'title’을 'All Meals’로 설정하고,
'description’을 'Browse the delicious meals shared by our vibrant community.'로 설정합니다.
5)동적 페이지의 메타데이터: 동적 페이지에서는 메타데이터가 불러오는 데이터에 따라 동적으로 바뀌어야 합니다. 이 경우, 정적 메타데이터를 등록하는 방법으로는 등록할 수 없습니다.
453.동적 메타데이터 추가
export async function generateMetadata({ params }){ const meal=getMeal(params.mealSlug); if(!meal){ notFound(); } return { title: meal.title, description: meal.summary, image: meal.image, } }
1)동적 페이지의 메타데이터: 동적 페이지에서는 'metadata’라는 이름의 변수나 상수를 export하는 것이 아니라, 'generateMetadata’라는 비동기 함수를 export하여 메타데이터를 등록합니다. 이 함수는 페이지 컴포넌트가 속성으로 받는 것과 동일한 데이터를 받습니다.
2)메타데이터 생성: ‘generateMetadata’ 함수는 'params’라는 이름으로 객체를 받아, 메타데이터를 만드는 데 필요한 데이터를 가져옵니다. 이 데이터를 이용하여 'title’과 'description’을 지정합니다.
3)메타데이터 오류 처리: 메타데이터는 처음에 만들어지기 때문에, 유효하지 않은 동적 페이지를 방문하면 에러 페이지가 나타납니다.
이 문제를 해결하기 위해, ‘generateMetadata’ 함수 안에서 'meal’에 값이 할당되었는지를 검사하고,
값이 없다면 ‘notFound’ 함수를 호출하여 not found 페이지가 보여지도록 합니다.
댓글 ( 0)
댓글 남기기