현재 Next.js 15.1.7 버전으로 작업 중인데요. dynamic route로 API 호출해 처리하는 페이지가 있고 페이지 호출 시 id 값으로 API 호출하는데 자료가 없는 경우 HTTP STATUS로 404 리턴을 해줘야 합니다.
일반적으로는 아래와 같은 코드로 작동할 수 있습니다. 예를 들어 app/pages/[id]/page.tsx 파일을 아래와 같이 작성합니다.
import { notFound } from 'next/navigation';
export default async function Page(props: {
params: Promise<{ id: number }>;
}) {
const pparams = await props.params;
const id = Number(pparams.id);
const data = await fetch(API_URI, { localdata_id });
if (!data) {
notFound();
}
return (<div>{JSON.stringify(data)}</div>)
}
/pages/112 이런 형태로 호출하면 정상적으로 200 리턴하며 페이지를 출력할 것입니다.
만약 999999 란 아이디가 없다면 /pages/999999 호출 시 notFound() 가 호출됩니다.
notFound() 의 return 형식은 never 입니다. 절대로 리턴이 없다는 의미이며 내부적으로 예외를 발생시키는 것으로 알고 있습니다.
결국 Next.js 에서 처리해서 정상적으로 404 리턴을 해줍니다.
문제점
하지만 아래와 같이 loading.tsx 를 넣어서 로딩 UI 나 스트리밍 처리를 넣게 되면 무조건 200 리턴만 가능합니다.
app/loading.tsx 를 아래와 같이 넣는다면…
'use client';
import CircularProgress from '@mui/material/CircularProgress';
import { useEffect, useState } from 'react';
export default function Loading() {
const [showLoading, setShowLoading] = useState(false);
useEffect(() => {
const timer = setTimeout(() => {
setShowLoading(true);
}, 500); // 0.5초 후에 로딩 페이지를 보여줌
return () => clearTimeout(timer); // 컴포넌트가 unmount 될 때 타이머 정리
}, []);
// 0.5초 이전에는 아무것도 렌더링 하지 않음
if (!showLoading) {
return null;
}
// You can add any UI inside Loading, including a Skeleton.
return (
<div className="page-content w-desktop flex flex-col items-center justify-center">
<CircularProgress />
<div>Loading...</div>
</div>
);
}
Next.js는 loading.tsx 파일 있는 경우 페이지 스트리밍 처리를 자동으로 시작하므로 무조건 200 리턴을 하고 페이지 로딩을 이어가기 때문에 중간에 notFound() 호출은 이미 버스가 떠난 뒤에 손을 흔드는 것과 같습니다.
따라서 loading UI 를 사용하면서도 404 처리를 하려면 middleware를 활용하는 수밖에 없습니다. 문제는 이때 미들웨어에서 먼저 API 를 fetch() 한 결과를 React.cache()로 캐싱 처리해도 React.cache() 는 서버 컴포넌트 내에서만 캐시 처리가 가능하기 때문에 Next.js 에서 제공하는 미들웨어서는 캐싱이 작동하지 않아 페이지 한 번 호출에 여러번 API 를 호출하게 됩니다. fetch() 옵션으로 캐싱을 넣을 수도 있지만 미들웨어에서 호출하는 것과 서버 컴포넌트에서 호출하는 것이 실제로는 헤더부분이 달라 다른 것으로 인식해 간단히 캐싱 처리가 되지 않습니다.
결국 Next.js 가 제공하는 기능인 loading.tsx를 제거하고 로딩 처리는 자체적으로 처리하는 형태로 변경해야 정상적으로 notFound() 가 작동해 404 처리가 가능해집니다.
$ flutter run
Launching lib/main.dart on SM S***N in debug mode...
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/FollowChannelHandlerActivity.kt:5:38 Unresolved reference: CustomTabsActivity
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/FollowChannelHandlerActivity.kt:6:5 'onNewIntent' overrides nothing
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/FollowChannelHandlerActivity.kt:7:15 Unresolved reference: onNewIntent
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/FollowChannelHandlerActivity.kt:11:9 Unresolved reference: setResult
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/FollowChannelHandlerActivity.kt:11:19 Unresolved reference: RESULT_OK
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/FollowChannelHandlerActivity.kt:12:9 Unresolved reference: finish
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/IntentFactory.kt:59:27 Unresolved reference: AuthCodeCustomTabsActivity
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:41:40 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:51:36 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:57:36 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:82:55 Unresolved reference: AuthCodeCustomTabsActivity
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:100:26 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:121:36 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:130:36 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:143:22 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:171:55 Type argument is not within its bounds: should be subtype of 'Activity'
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:218:49 Unresolved reference: IntentResolveClient
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:237:21 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:258:21 Unresolved reference: Utility
e: file:///home/doogle/.pub-cache/hosted/pub.dev/kakao_flutter_sdk_common-1.8.0/android/src/main/kotlin/com/kakao/sdk/flutter/KakaoFlutterSdkPlugin.kt:274:40 Unresolved reference: Utility
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':kakao_flutter_sdk_common:compileDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
> Compilation error. See log for more details
* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
* Get more help at https://help.gradle.org
BUILD FAILED in 8s
Running Gradle task 'assembleDebug'... 8.9s
Exception: Gradle task assembleDebug failed with exit code 1
갑자기 에러가 나서 구글링해보는데 app/build.gradle 파일에 있는 ex.kotlin_version 을 올려보라는 말이 있어서 해보니 정상적으로 빌드가 됩니다. 휴…
buildscript {
ext.kotlin_version = '1.9.21'
...
}
위 ext.kotlin_version = ‘1.9.21’ 부분을 ext.kotlin_version = ‘1.9.22’ 로 변경하고 다시 flutter run 을 실행해보니 잘 빌드되네요. 순간 당황했습니다.
/var/lib/docker 폴더의 용량이 매우 크게 증가해서 서버가 용량 문제로 죽어버리는 경우가 발생한 것인데요. /var 가 포함된 / 파티션 용량 자체가 작게 해둔 문제도 있지만 이상하게 docker 경로가 용량이 폭증하는 문제의 원인을 알 수 없더군요.
현재 확실한 원인은 찾지 못했지만 잠정적으로는 컨테이너 내에 의도치 않게 쌓이는 데이터들이 문제라고 의심 중입니다. 특히 로그 파일이나 세션 파일 등입니다.
그래서 일단 PHP에서 파일세션을 사용하는데 이를 Redis로 전환했습니다.
일단 웹페이지 로딩 속도도 개선되고 좋습니다. 근데 세션 때문에 용량이 과다하게 증가하는 문제가 모두 해결된 거 같지는 않았습니다.
더 찾아보니 제가 한 컨테이너에 2가지 데몬을 실행시키려고 supervisord 서비스를 이용하고 있는데 이 설정에 문제가 있는 것 같았습니다.
기존 설정은 아래와 같습니다.
; supervisor config file
[unix_http_server]
file=/var/run/supervisor.sock ; (the path to the socket file)
chmod=0700 ; sockef file mode (default 0700)
[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP)
nodaemon=true
user=root
; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket
; The [include] section can just contain the "files" setting. This
; setting can list multiple files (separated by whitespace or
; newlines). It can also contain wildcards. The filenames are
; interpreted as relative to this file. Included files *cannot*
; include files themselves.
[include]
files = /etc/supervisor/conf.d/*.conf
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
umask=0
[program:php-fpm5.6]
command=/usr/sbin/php-fpm5.6
autostart=true
autorestart=true
umask=0
이렇게 해서 Dockerfile에서 적용해 빌드 해서 한 컨테이너에 Nginx 와 PHP-FPM 서비스를 같이 돌리고 있습니다.
# docker compose build
# docker compose up -d
이렇게 빌드 후 실행해서 프로세스 상태를 보면 아래와 같이 조금 이상한 부분이 있습니다.
php-fpm 이 좀 이상합니다.
[php-fpm5.6] <defunct>로 supervisord 에서 실행된 php-fpm 프로세스는 죽어 있고 별도로 php-fpm 프로세스가 떠있습니다.
로그들을 분석하다 보니 php-fpm5.6 이 계속 이미 따로 떠있어서 실행할 수 없다고 supervisord의 로그 쪽에 나오는거 같네요. 위 설정에서 autorestart 설정이 있기때문에 죽은 php-fpm5.6 프로세스를 계속 재시작하는 것 같습니다.
결론은 docker 컨테이너나 supervisord 에서도 데몬으로 띄우지 않아야 정상적으로 작동하는데 데몬형태로 서비스를 띄워버려서 문제가 된 것입니다.
일단 위의 superviserd.conf 파일을 수정해야 합니다.
command=/usr/sbin/php-fpm5.6
;이부분을 다음과 같이 변경합니다.
command=/usr/sbin/php-fpm5.6 -F
이렇게 하면 데몬으로 뜨지 않고 일반 프로세스로 실행됩니다.
여기에 로그내용을 docker에서 확인할 수 있도록 supervisor.conf에 각 서비스 별로 아래 로그 저장을 stdout, stderr로 보내도록 설정합니다.
; supervisor config file
[unix_http_server]
file=/var/run/supervisor.sock ; (the path to the socket file)
chmod=0700 ; sockef file mode (default 0700)
[supervisord]
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
childlogdir=/var/log/supervisor ; ('AUTO' child log dir, default $TEMP)
nodaemon=true
user=root
; the below section must remain in the config file for RPC
; (supervisorctl/web interface) to work, additional interfaces may be
; added by defining them in separate rpcinterface: sections
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
[supervisorctl]
serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket
; The [include] section can just contain the "files" setting. This
; setting can list multiple files (separated by whitespace or
; newlines). It can also contain wildcards. The filenames are
; interpreted as relative to this file. Included files *cannot*
; include files themselves.
[include]
files = /etc/supervisor/conf.d/*.conf
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
umask=0
; 로그파일을 만들어서 컨테이너에 쌓이도록 하면 안된다. 표준 출력으로 내보낸다.
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:php-fpm5.6]
command=/usr/sbin/php-fpm5.6 -F
;command=/usr/sbin/php-fpm5.6 --nodaemoize --fpm-config=/etc/php/5.6/fpm/pool.d/www.conf
autostart=true
autorestart=true
umask=0
; 로그파일을 만들어서 컨테이너에 쌓이도록 하면 안된다. 표준 출력으로 내보낸다.
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
이제 이렇게 변경 후 재 빌드 해서 사용하면 php-fpm 도 foreground process 로 떠서 superviserd 가 잡고 있고 로그도 docker logs 등의 명령으로 확인할 수 있습니다.
오랜만에 Nginx 와 php-fpm 환경 조합을 하는데 하필 *.html 확장자 파일에서도 PHP 구문이 실행되어야 정상적으로 웹사이트가 돌아가는 문제가 있었습니다. 안그러면 단순 html 문서로만 인식해서 <?php … ?> 등 php 소스 부분도 그대로 노출되고 제대로 페이지가 출력되지 않습니다.
댓글을 달려면 로그인해야 합니다.