Next.js와 Node.js를 이용한 일기 작성 및 감정 분석 앱 애플리케이션 튜토리얼
by 담담이담해당 글은 네이버클라우드의 기술 콘텐츠 리워드 프로그램 2024년 5월의 Nclouder로 선정된 포스팅입니다.
https://blog.naver.com/n_cloudplatform/223477261634
이 튜토리얼에서는 Next.js와 Node.js를 사용하여 일기 작성 및 감정 분석 애플리케이션을 로컬에서 실행하는 방법을 단계별로 설명합니다. 사용자는 일기를 작성하고 제출하면 감정 분석 결과를 나무로 확인할 수 있습니다. 이 프로젝트는 Naver Sentiment API를 이용하여 감정을 분석합니다.
저는 이 프로젝트에서 프론트엔드 개발을 맡았지만 프론트엔드 개발 뿐만 아니라 백엔드 개발에도 참여했습니다.
목차
1. 프로젝트 개요
2. 프로세스 흐름을 순서대로 요약
1) Next.js 프론트엔드
2) Node.js 백엔드
3) Naver Sentiment API
4) Node.js 백엔드
5) Next.js 프론트엔드
3. 통신 방법 상세
1) 프론트엔드 (Next.js)
- Axios 인스턴스 설정
- 일기 제출 함수
2) 백엔드 (Node.js)
- 서버 설정
- 감정 분석 라우트
- 서버 시작
4. 프론트엔드 코드 설명
1) HomePage 컴포넌트
- Dynamic Import
- ReactQuill 라이브러리 사용
- 상태 관리
- handleSubmit 함수
5. Naver Sentiment API 응답 타입 정의
1) Naver Sentiment API 응답 예시
2) 타입 정의
- Confidence 타입
- Highlight 타입
- Sentence 타입
- Document 타입
- ResponseData 타입
3) API 호출 함수
4) 결론
6. 실행방법 및 실행결과
7. 마무리
1. 프로젝트 개요
- Next.js(App Router), TypeScript: 프론트엔드 프레임워크
- Node.js: 백엔드 서버
- Naver Sentiment API: 감정 분석을 위한 API
2. 프로세스 흐름을 순서대로 요약
1) Next.js 프론트엔드
- 일기 작성 (텍스트 에디터)
- 제출하기 버튼 클릭 → Node.js 서버로 API 호출
2) Node.js 백엔드
- Next.js 프론트엔드로부터 일기 내용 수신
- Naver Sentiment API로 감정 분석 요청
3) Naver Sentiment API
- 감정 분석 수행 후 결과를 Node.js 서버로 응답
4) Node.js 백엔드
- 감정 분석 결과를 Next.js 프론트엔드로 응답
5) Next.js 프론트엔드
- 감정 분석 결과 수신 및 나무로 결과값을 시각화 화면 표시
3. 통신 방법 상세
Next.js와 Node.js를 사용하여 로컬 환경에서 일기 작성 및 감정 분석 애플리케이션을 통신하는 방법을 설명하겠습니다. 여기서 Next.js는 프론트엔드 서버로 3000번 포트를 사용하고, Node.js는 백엔드 서버로 3001번 포트를 사용하여 데이터를 주고받습니다.
1) 프론트엔드 (Next.js)
프론트엔드에서는 사용자가 일기를 작성하고 이를 제출하면, 작성된 일기 내용이 Node.js 서버로 전송됩니다. 이를 위해 Axios를 사용하여 HTTP 요청을 처리합니다.
- Axios 인스턴스 설정
먼저, Axios 인스턴스를 생성하여 기본 설정을 지정합니다. 이 인스턴스는 Node.js 서버와 통신할 때 사용됩니다.
import axios from "axios"; export const instance = axios.create({ baseURL: "<http://localhost:3001>",// Node.js 서버 주소 headers: { "Content-Type": "application/json", }, }); // 항상 패칭 요청을 보내면 response.data가 값이 리턴되게 instance.interceptors.response.use( function (response) { return response.data; }, // 에러 일괄 처리async (error) => { console.log(error.message); return Promise.reject(error); }, );
- 일기 제출 함수
작성된 일기 내용을 Node.js 서버로 전송하는 함수를 정의합니다. 이 함수는 일기 내용을 받아 POST 요청을 통해 서버로 전달합니다.
import { instance } from "@/api/axiosInstance"; export const postDiary = async (content: string): Promise<ResponseData> => { return await instance.post("/diary", { content, }); };
2) 백엔드 (Node.js)
백엔드에서는 프론트엔드로부터 전송된 일기 내용을 받아 Naver Sentiment API로 요청을 보내고, 감정 분석 결과를 다시 프론트엔드로 전달합니다. 이 과정은 Express를 사용하여 처리합니다.
- 서버 설정
Express를 사용하여 서버를 설정하고 필요한 미들웨어를 적용합니다.
import express from "express"; import bodyParser from "body-parser"; import axios from "axios"; import cors from "cors"; const app = express(); const port = 3001; // CORS 설정 app.use(cors()); // body-parser 미들웨어 설정 app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true }));
- 감정 분석 라우트
일기 내용을 받아 Naver Sentiment API로 요청을 보내는 라우트를 설정합니다. 요청이 성공하면, 결과를 프론트엔드로 전달합니다.
app.post("/diary", async (req, res) => { const { content } = req.body; const client_id = "YOUR_CLIENT_ID"; const client_secret = "YOUR_CLIENT_SECRET"; const url = "<https://naveropenapi.apigw.ntruss.com/sentiment-analysis/v1/analyze>"; const headers = { "X-NCP-APIGW-API-KEY-ID": client_id, "X-NCP-APIGW-API-KEY": client_secret, "Content-Type": "application/json", }; try { console.log("Request received:", { content });// 디버깅을 위한 로그 추가const response = await axios.post(url, { content }, { headers }); console.log("Naver API response:", response.data);// 디버깅을 위한 로그 추가 res.json(response.data); } catch (error) { console.error("Naver API error:", error.response ? error.response.data : error.message);// 디버깅을 위한 로그 추가 res.status(error.response ? error.response.status : 500).json({ error: error.message }); } });
- 서버 시작
Node.js 서버를 3001번 포트에서 실행합니다.
app.listen(port, () => { console.log(`Server is running on <http://localhost>:${port}`); });
4. 프론트엔드 코드 설명
1) HomePage 컴포넌트
HomePage 컴포넌트는 TextEditor를 사용하여 사용자가 일기를 작성하고, 작성된 일기를 제출하여 감정 분석 결과를 보여줍니다.
"use client"; import React, { useState, useRef } from "react"; import dynamic from "next/dynamic"; import ReactQuill from "react-quill"; import { postDiary, ResponseData } from "@/api/postDiary"; import TreeCanvas from "@/components/tree/TreeCanvas"; const TextEditor = dynamic(() => import("@/components/Home/TextEditor"), { ssr: false, }); export default function HomePage() { const quillRef = useRef<ReactQuill | null>(null); const [htmlContent, setHtmlContent] = useState<string>(""); const [response, setResponse] = useState<ResponseData>(); const [showTree, setShowTree] = useState<boolean>(false); const [hp, setHp] = useState<number>(0); const handleSubmit = async () => { const res = await postDiary(htmlContent); setResponse(res);// 전체 응답 객체 저장 setHp(res.document.confidence.positive);// hp 값을 상태로 설정 setShowTree(true);// TreeCanvas를 보여주도록 설정 }; return ( <div className="min-h-screen flex flex-col items-center justify-center bg-gray-100 gap-30pxr"> <h1 className="text-4xl font-bold mb-8 text-center">유담이의 일기</h1> <div className="mb-4 w-full"> <TextEditor quillRef={quillRef} htmlContent={htmlContent} setHtmlContent={setHtmlContent} /> </div> <div className="w-full flex justify-center"> <button onClick={handleSubmit} className="font-bold py-2 px-4 rounded"> 제출하기 </button> </div> {response && ( <div className="mt-4 p-4 bg-white rounded shadow"> <h2 className="text-2xl font-bold mb-2">응답값</h2> <pre>감정: {response.document.sentiment}</pre> <pre>부정: {response.document.confidence.negative.toFixed(2)}%</pre> <pre>긍정: {response.document.confidence.positive.toFixed(2)}%</pre> <pre>중립: {response.document.confidence.neutral.toFixed(2)}%</pre> </div> )} {showTree && ( <div className="mt-8 w-full flex justify-center"> <TreeCanvas hp={hp} day={true} /> </div> )} </div> ); }
- Dynamic Import: TextEditor를 동적으로 임포트하여 클라이언트 측에서만 렌더링되도록 설정합니다.
const TextEditor = dynamic(() => import("@/components/Home/TextEditor"), { ssr: false, });
dynamic 함수를 사용하여 TextEditor 컴포넌트를 동적으로 임포트하는 이유는 서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR) 간의 호환성을 높이기 위해서입니다. Next.js는 기본적으로 SSR을 사용하여 페이지를 렌더링하는데, ReactQuill 같은 일부 라이브러리는 클라이언트 측에서만 동작합니다. 따라서, 이러한 라이브러리를 사용하는 컴포넌트를 동적으로 로드하여 클라이언트 측에서만 렌더링되도록 설정하는 것이 필요합니다.
- ReactQuill 라이브러리 사용
ReactQuill은 Quill.js 기반의 리치 텍스트 에디터 라이브러리입니다. 사용자가 텍스트를 포맷팅하고 스타일을 적용할 수 있는 WYSIWYG 에디터를 제공합니다. 저는 Html에서 기본으로 제공하는 textarea 대신 ReactQuill 기반의 TextEditor 라이브러리를 선택하였습니다. 이를 통해 사용자는 더욱 풍부하고 개인화된 일기 작성 경험을 누릴 수 있으며, 다양한 포맷팅 옵션과 기능을 활용하여 자신만의 스타일로 콘텐츠를 작성할 수 있습니다. TextEditor 라이브러리는 사용자 경험을 향상시킬 것이라고 생각합니다.
- 상태 관리
htmlContent, response, showTree, hp 등의 상태를 관리하여 UI를 업데이트합니다.
- handleSubmit 함수: 작성된 일기를 서버에 제출하고 응답 결과를 상태로 저장합니다.
const handleSubmit = async () => { const res = await postDiary(htmlContent); setResponse(res); // 전체 응답 객체 저장 // 응답 값에서 필요한 데이터를 추출 setHp(res.document.confidence.positive); // hp 값을 상태로 설정 setShowTree(true); // TreeCanvas를 보여주도록 설정 };
5. Naver Sentiment API 응답 타입 정의
이 프로젝트에서는 Naver Sentiment API의 응답 데이터를 기반으로 TypeScript 타입을 정의하여, API 응답을 더 명확하게 처리하고 안전하게 사용할 수 있도록 했습니다.
1) Naver Sentiment API 응답 예시
Naver Sentiment API의 응답 예시는 다음과 같습니다:
Request received: { content: '<p><span style="color: rgb(163, 21, 21);">날씨는 어제처럼 우중충하고, 마음도 그런 느낌이다. 하루가 시작되면서부터 뭔가 잘못된 것 같은 예감이 들었는데, 그 예감은 현실이 되어버렸다. 아침에 일어나자마자 느껴지는 무기력함과 좌절감이 마치 몸과 마음을 휩쓸어 가는 듯했다 하루가 저물어가는 동안에도 내 안의 불안과 무력함은 사그라들지 않았다. 하나 둘 쌓이는 문제들이 마치 나를 짓누르듯 무거웠다. 어떻게 해결해야 할지 모를 때, 그냥 문제들을 무시하고 싶은 마음이 더 커졌다. 주변에 있는 사람들도 마찬가지였다. 서로를 이해하고 지지해 줄 줄도 모르는 것 같았다. 소통도 잘 되지 않았고, 서로에 대한 이해와 배려가 부족했다. 그저 각자가 자신의 문제와 고민에 치여 살아가는 것 같았다. 이런 날에는 정말로 힘이 들었다. 내일은 조금이라도 나아질 수 있기를 바라며 오늘을 마무리하려고 한다. 하지만 지금의 마음이라면 내일도 비슷한 하루가 될 것만 같다. 현실을 직시하고 견뎌내는 것이 얼마나 힘든 일인지 다시 한번 느꼈다.</span></p>' } Naver API response: { document: { sentiment: 'negative', confidence: { negative: 92.29367, positive: 0.00500663, neutral: 7.7013206 } }, sentences: [ { content: '<p><span style="', offset: 0, length: 16, sentiment: 'neutral', confidence: [Object], highlights: [Array] }, { content: 'color: rgb(163, 21, 21);">날씨는 어제처럼 우중충하고, 마음도 그런 느낌이다.', offset: 16, length: 54, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 하루가 시작되면서부터 뭔가 잘못된 것 같은 예감이 들었는데, 그 예감은 현실이 되어버렸다.', offset: 70, length: 51, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 아침에 일어나자마자 느껴지는 무기력함과 좌절감이 마치 몸과 마음을 휩쓸어 가는 듯했다', offset: 121, length: 48, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 하루가 저물어가는 동안에도 내 안의 불안과 무력함은 사그라들지 않았다.', offset: 169, length: 40, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 하나 둘 쌓이는 문제들이 마치 나를 짓누르듯 무거웠다.', offset: 209, length: 31, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 어떻게 해결해야 할지 모를 때, 그냥 문제들을 무시하고 싶은 마음이 더 커졌다.', offset: 240, length: 45, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 주변에 있는 사람들도 마찬가지였다.', offset: 285, length: 20, sentiment: 'neutral', confidence: [Object], highlights: [Array] }, { content: ' 서로를 이해하고 지지해 줄 줄도 모르는 것 같았다.', offset: 305, length: 29, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 소통도 잘 되지 않았고, 서로에 대한 이해와 배려가 부족했다.', offset: 334, length: 35, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 그저 각자가 자신의 문제와 고민에 치여 살아가는 것 같았다.', offset: 369, length: 34, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 이런 날에는 정말로 힘이 들었다.', offset: 403, length: 19, sentiment: 'negative', confidence: [Object], highlights: [Array] }, { content: ' 내일은 조금이라도 나아질 수 있기를 바라며 오늘을 마무리하려고 한다.', offset: 422, length: 39, sentiment: 'neutral', confidence: [Object], highlights: [Array] }, { content: ' 하지만 지금의 마음이라면 내일도 비슷한 하루가 될 것만 같다.', offset: 461, length: 35, sentiment: 'neutral', confidence: [Object], highlights: [Array] }, { content: ' 현실을 직시하고 견뎌내는 것이 얼마나 힘든 일인지 다시 한번 느꼈다.</span></p>', offset: 496, length: 50, sentiment: 'neutral', confidence: [Object], highlights: [Array] } ] }
이 응답 구조를 바탕으로 TypeScript 타입을 정의했습니다.
2) 타입 정의
interface Confidence { negative: number; positive: number; neutral: number; } interface Highlight { offset: number; length: number; } interface Sentence { content: string; offset: number; length: number; sentiment: string; confidence: Confidence; highlights: Highlight[]; } interface Document { sentiment: string; confidence: Confidence; } export interface ResponseData { document: Document; sentences: Sentence[]; }
- Confidence 타입
confidence 객체는 negative, positive, neutral 세 가지 속성으로 구성되어 있으며, 각각의 속성은 숫자 값을 가집니다.
- Highlight 타입
highlight 객체는 문장의 특정 부분을 강조하기 위한 offset과 length 속성으로 구성됩니다.
- Sentence 타입
sentence 객체는 개별 문장을 나타내며, content, offset, length, sentiment, confidence, highlights 속성을 가집니다. 이 속성들은 문장의 내용, 위치, 길이, 감정, 신뢰도, 하이라이트 정보를 포함합니다.
- Document 타입
document 객체는 전체 문서의 감정과 신뢰도 정보를 포함합니다.
- ResponseData 타입
최종적으로, API 응답 전체를 나타내는 ResponseData 타입은 document와 sentences 배열을 포함합니다.
3) API 호출 함수
이 타입들을 사용하여, API 호출 함수를 정의합니다. 이 함수는 작성된 일기를 서버로 보내고, 서버에서 감정 분석 결과를 받아와서 ResponseData 타입으로 반환합니다.
import { instance } from "@/api/axiosInstance"; export const postDiary = async (content: string): Promise<ResponseData> => { return await instance.post("/diary", { content, }); };
4) 결론
이와 같이 TypeScript 타입을 정의함으로써, API 응답 데이터를 보다 명확하게 처리하고 타입 안전성을 보장할 수 있습니다. 이러한 타입 정의는 코드의 가독성을 높이고, 유지보수를 용이하게 합니다.
6. 실행 방법 및 실행 결과
저희 팀이 만든 애플리케이션을 로컬 환경에서 실행하는 방법을 설명하겠습니다. 사용자가 일기를 작성하고, 제출하면 일기 내용에 대한 감정 분석 결과를 나무 그림으로 시각화하여 보여줍니다.
1) 프론트엔드(Next.js) 실행하기
프론트엔드 애플리케이션은 Next.js 프레임워크를 사용합니다. 애플리케이션을 실행하기 전에, 프로젝트의 루트 디렉토리에서 필요한 npm 패키지들을 설치해기 위해 npm install 명령어를 실행합니다. 패키지들이 성공적으로 설치된 후, npm run dev라는 명령어로 Next.js 서버를 시작할 수 있습니다. 이 명령은 개발 모드로 Next.js 애플리케이션을 실행시키며, 기본적으로 `http://localhost:3000`에서 애플리케이션에 접근할 수 있습니다.
npm install npm run dev

2) 백엔드(Node.js) 실행하기
백엔드 서버 또한 의존성 설치가 필요하며, 백엔드 디렉토리에서 npm install이라는 명령어를 실행하여 설치할 수 있습니다. 의존성 설치 후, node server.js라는 명령어로 Node.js 서버를 실행합니다. 여기서 server.js는 파일 이름을 의미합니다.
npm install node server.js
이 명령은 `http://localhost:3001`에서 백엔드 서버를 실행합니다. 프론트엔드에서는 이 주소로 API 요청을 보내게 됩니다.

3) 애플리케이션 사용하기
서버가 실행되고 나면, 웹 브라우저를 열고 `http://localhost:3000`으로 이동합니다. 사용자 인터페이스에서 일기를 작성하고, '제출하기' 버튼을 클릭합니다. 일기 내용이 백엔드로 전송되고, 감정 분석을 수행한 결과가 나무 이미지로 변환되어 화면에 표시됩니다.
4) 결과
감정 분석의 결과를 시각적으로 잘 표현하고 있으며, 사용자가 긍정적, 중립적, 혹은 부정적 감정을 얼마나 느꼈는지를 나타냅니다.

7. 마무리
이 튜토리얼을 통해 우리는 Next.js와 Node.js를 사용하여 일기 작성 및 감정 분석 애플리케이션을 구현하는 방법을 학습했습니다. 특히, Naver Sentiment API를 사용하여 감정 분석 기능을 추가하고, TypeScript를 통해 API 응답 데이터를 안전하게 처리하는 방법을 다뤘습니다.

블로그의 정보
유명한 담벼락
담담이담