Next.js 14와 mongodb 사용 시 Only plain objects can be passed to... 경고 메세지 해결

Shyuuuuni·2024년 1월 7일
0

❌ Trouble-shooting

목록 보기
8/9

문제상황

Next.js 14버전부터 서버 액션이 정식 기능에 포함되면서, 이를 토이 프로젝트에서 사용해보고 있다.

문제 상황은 서버 컴포넌트에서 클라이언트 컴포넌트로 서버 액션을 전달하고, 서버 액션에서 mongodb에 접근하여 문서를 검색하고 그 결과를 반환하는 코드에서 발생했다.

오류 메세지

Warning: Only plain objects can be passed to Client Components from Server Components. Objects with toJSON methods are not supported. Convert it manually to a simple value before passing it to props.
  {_id: {}, id: ..., comment: ..., updatedAt: ..., expireAt: ...,  }
        ^^

소스코드

서버 컴포넌트 (/auth/page.tsx)

사용자 인터렉션이 일어나는 부분을 클라이언트 컴포넌트로 분리하고, 서버 액션을 props로 전달한다.

// /app/auth/page.tsx
import * as styles from '@/app/auth/AuthPage.css';

import RegisterCertificationSection from '@/app/auth/_component/RegisterCertificationSection';
import { getCertification } from '@/app/auth/actions';

export default async function AuthPage() {
	return (
		<div>
        	{/* ... */}
			<div className={styles.registerCertificationZone}>
              	{/* ... */}
				<RegisterCertificationSection actions={{ getCertification }} />
			</div>
		</div>
	);
}

서버 액션

apiKey 입력 -> id 계산 -> 데이터베이스 조회 순서로 작동하는 서버 액션 함수를 작성한다.

// /app/auth/actions
'use server';

import { findCertification } from '@/app/auth/database';

export const getCertification = async (apiKey: string) => {
	const ouid = await fetchOuid(apiKey);
	const id = hashOuid(ouid);
	const certification = await findCertification(id);

	return certification; // WithId<Certification> | null
};
// /app/auth/database.ts
'use server';

import { Certification } from '@/app/_model/maplestory/certification';
import mongoClient from '@/app/_config/database';

export const findCertification = async (userId: string) => {
	const database = (await mongoClient).db('데이터베이스이름');
	const collection = database.collection<Certification>('certifications');
	const certification = await collection.findOne({ id: userId });

	return certification; // WithId<Certification> | null
};

클라이언트 컴포넌트

사용자에게서 API Key를 입력받고, 폼을 제출할 때 서버에서 해당 키와 동일한 인증서를 가져온다.

'use client';

import * as styles from './RegisterCertificationSection.css';
import { ChangeEventHandler, FormEventHandler, useState } from 'react';
import { Certification } from '@/app/_model/maplestory/certification';

export default function RegisterCertificationSection({ actions }: Props) {
	const [apiKeyInput, setApiKeyInput] = useState(nxopenApiKey);

	const onSubmitApiKey: FormEventHandler<HTMLFormElement> = async (e) => {
      	// ...중간 로직 생략
		const certification = await actions.getCertification(apiKeyInput);
      	console.log(certification);
	};

	return (
		<div>
        	{/* ... */}
			<form className={styles.validationForm} onSubmit={onSubmitApiKey}>
				<input
					className={styles.apiKeyInput}
					value={apiKeyInput}
					onChange={onChangeApiKey}
					placeholder="API Key를 입력해주세요."
				/>
				<button className={styles.searchButton} type="submit">
					검색하기
				</button>
			</form>
		</div>
	);
}

해결방법

mongodb의 findOne()의 결과로 WithId<T> 타입의 데이터가 반환된다.

WithId<T> 속에는 _id 라는 필드가 자동으로 ObjectId 클래스의 인스턴스를 반환한다.

에러 메세지에 나와 있는 것 처럼, 순수한 자바스크립트 객체만을 넘겨줄 수 있기 때문에 이러한 경고 메세지가 발생한다.

간단하게 certification._id = certification._id.toString() 과 같이 처리해도 되지만, 설정에 따라 타입스크립트 경고 메세지가 표시된다.

나는 별도의 withStringId라는 함수를 구현하였고, 서버에서 데이터를 반환할 때 감싸주도록 코드를 수정했다.

// /app/_utils/database/withStringId.ts
import { WithId } from 'mongodb';

export type WithStringId<T> = Omit<WithId<T>, '_id'> & {
	_id: string;
};
type FindResult<T> = WithId<T> | null;

const withStringId = <T>(result: FindResult<T>): WithStringId<T> | null => {
	if (result === null) {
		return null;
	}

	const { _id, ...rest }: WithId<T> = result;
	return {
		_id: _id.toString(),
		...rest,
	};
};

export default withStringId;

여기서 값을 반환하는 부분을 데이터베이스에 접근하는 함수 자체에서 처리할 수 있다. 하지만 데이터베이스에 접근하는 코드는 해당 기능만 담당하고, 이를 활용하고 응답하는 서버 액션 함수에서 감싸는게 더 좋다고 생각했다.

// /app/auth/actions.ts
'use server'

import { findCertification } from '@/app/auth/database';
import { withStringId } from '@/app/_utils/database';

export const getCertification = async (apiKey: string) => {
	const ouid = await fetchOuid(apiKey);
	const id = hashOuid(ouid);
	const certification = await findCertification(id);

	return withStringId(certification);
};

마무리

  • 추가로 mongoose를 사용한다면 조회 이후 lean()이라는 메소드를 통해 단순화 시킬 수 있다고 한다. 이번 문제도 동일하게 해결할 수 있지 않을까?
profile
배짱개미 개발자 김승현입니다 🖐

0개의 댓글