요즘은 시도중인 것들중 가장 큰거는

 

현재 일하고있는 회사에서는 php 버전이 5.*, 7.*, 8.* 등등 다양하게 널뛰기를 한다.

그리고 레거시 코드를 사용하고 있다.

 

그러다보니 레거시 코드 프로젝트간의 코드 통일이 안되고 있다

5.* 버전과 코드 스타일과 7.*, 8.* 의 코드 스타일이 완전 다르다.

그러다보니 코드 유지보수에 어려움을 겪고있고 코드를 읽어내는 시간이 지연되고 있다.

 

그리고 가장 큰 문제가 레거시다보니 프레임워크에서는 당연히 지원되는 개발모드, 배포모드 구분이 없다.

그래서 쿼리 에러가 나오면 사용자에게 에러메시지가 모두 노출된다. 

 

코드스타일이 가장 차이 나는부분이 특히 select, insert, update, delete 에서 차이가 많이난다.

가장 간단한건 프레임워크를 도입하고 리펙토링을 진행하는거다. 

그런데 마음처럼 쉽지않다. 

 

프레임워크를 도입하고 리펙토링을 진행하는 자체가 기간소요가 발생하고

이건 기업 입장에서는 수익이 0이니 할 이유가 없다.

 

그럼 프레임워크 도입은 깔끔하게 던져둔다.

 

그럼 챗gpt를 고문시켜서 통합 펑션을 만드는 중이다.

만드는건 거의 끝나서 테스트 중에 있고

어떤 상황에서든지 php 버전과 mysql 버전 상관없이 해당 펑션을 사용해서 쿼리를 보낼 수 있도록 만들고 있다.


select 펑션

매개변수

sql 쿼리를 쌩으로 받는 변수

, 쌩으로 받더라도 사용자 입력으로 select 시에 ? 로 대체한 값을 담는 배열

, 반환 타입 지정(1개, 배열) 

, 쿼리 에러시 디비에 저장되는 구분 메모 문자

 

이정도만 받고 만약 쿼리 에러시 에러를 리턴하지 않고 문제가 되는 쿼리를 저장하고 다른 컬럼에 구분메모문자까지 저장한다. 그리고 사용자에게 익숙한 실패메시지만 리턴을 한다.

 


여기서 조금만 더 나가보면 리스트를 리턴할때 당연히 페이징을 한다.

 

그럼 페이징 펑션도 필요하다.

 

페이징 펑션은 select 펑션을 안에서 호출하는 구조인데 추가로 받는 매개변수는 페이지열수, 페이지수, 이것 두개를 더 받는다. 이렇게 해서 select 펑션에 던지는 sql 처럼 똑같이 던지는 변수를 받으면 그걸 한번 감싸서 count 를 해서 데이터 최대 갯수를 세고 최대 갯수만큼 페이지 열수를 나눠서 페이징 처리를 한다.

 

이렇게만 만들면 단순 유지보수에는 문제가 없다. 하지만 데이터 갯수가 500만건 정도 넘어버리니 이 펑션처리속도가 너무 느리게 흘러가는 문제가 있다. 문제는 전체 열 카운트에 있었다. 이걸 좀 더 최적화해서 발전하기에는 쿼리튜닝을 해야하는데 튜닝을해도 복잡한 쿼리가 등장하면 의미가 사라졌다. 그래서 방법은 그냥 데이터가 페이지 열수의 10배가 있다 치고 페이징을 지속 하는거다. 이렇게하면 최대 데이터수를 체크를 조건에 따라 바로 확인을 못하는 단점이 있는데 이부분은 어떻게 해결 가능할지 계속 고민중이다. 

 


insert 펑션

매개변수

테이블명

, 추가할 연관배열 데이터

, 쿼리 에러시 디비에 저장되는 구분 메모 문자

 

이렇게 구현했다. insert 는 따로 조건이 들어가고 그런게 아니라서 생각보다 펑션 구현이 단순하다.

 


update 펑션

매개변수

테이블명

, 추가할 연관배열 데이터

, 조건

, 쿼리 에러시 디비에 저장되는 구문 메모 문자

 

delete 보다 update 를 더 많이 사용해서 update 에 많이 신경을 썼다.

만약 추가할 데이터가 [ 'user_id' => 'my_ids' ] 이런 형식으로만 들어온다면 참 좋을거같다.

하지만 코드를 짜고 유지보수를 하면 증감을 하거나 연산을 할 필요가 있다. 이럴때는 update 를 보내기전에 select를 해서 데이터를 받아오고 그걸 더해서 업데이트를 하도록 코드 구현이 대부분 되어왔다. 복잡한 계산을 한다면 이렇게 하는게 코드 유지보수에는 훨씬 좋긴하다. 하지만 단순 연산일때도 이렇게하면 select 를 한번 더 하기때문에 자원소모가 있다.

 

[ 'user_cnt' => [ '+', 1 ] ]

이렇게 매개변수를 보내면 펑션내부적으로 처리해서 user_cnt 를 1을 더하는 쿼리를 보낸다. 

그리고 조건에서 단순하게

where user_id = 'my_id'

이렇게 보낸다면 참 이상적이다. 

거의 모든 상황에서 지원하는 펑션을 만드는 목적이라면 그러지 않는다는걸 잘알고 있다.
그럼 조건을 ['user_id' => 'my_id'] 로 보내고 있다면 쌩 문자열을 보내면 그 문자열의 조건으로 실행되도록 예외를 추가했다.


delete 펑션

매개변수

테이블명

, 조건

, 쿼리 에러시 디비에 저장되는 구문 메모 문자

 

updte 펑션과 동일하게 조건 매개변수 로직을 구현했다. 


 

이런식으로 거의 모든 상황에서 사용가능한 펑션을 만들었다. 이번에 경험해보면서 느낀게 이때까지 프레임워크에 의존해서 쿼리를 보내왔는데 이번에 펑션으로 구현해보면서 mysql 쿼리 동작코드도 경험할 수 있고 좋았다. 조금 더 이래저래 써보면서 추가 보안을 계속 해봐야겠다. 조금 구색이 갖춰지면 코드를 보면서 구현의도 이런것들을 좀 더 자세하게 기록하면 더 좋을거같다 

 

 

 

 

'일상' 카테고리의 다른 글

trongate ? 간단후기  (1) 2024.09.14
내가 생각하는 아키텍처 패턴?  (0) 2024.07.28
HMVC 패턴?  (0) 2024.07.28

 

 

trongate 프레임워크를 사용해봤는데 주기적으로 수정 발전이 일어나서 지금의 경험이 나중에가서는 더 나아지거나 변했을 가능성이 크지만 지금까지의 경험을 정리하자면 이렇다.

 

 

1. 우선 속도가 빠르다.

2. 관리자 페이지를 지원한다.

관리자가 서비스 관리자가 아닌 프로젝트 관리자였다. db 설정 등등 이라서 조금 둘러보다가 말았다. 

3. 프론트 코드 방식을 htmx 에서 영감을 받아서 trongate mx 를 만들어서 사용한다.

이덕분에 htmx 를 처음 접했는데 코드자체는 간결하고 보기 좋았다. 하지만 최대 단점이라 하면 버튼 클릭으로 get 요청을 서버에 했다면 그 이벤트 처리를 다 끝내고 리턴을 html 로 해야한다는거다. 서버에서 html 코드를 만들어서 리턴 해야한다. 이게 뭔가 이상했다. html 코드를 서버에서 관리를한다는게 이상했다. 서버코드와 프론트 코드를 분리를해서 코드 유지보수를 하고싶은데 이게 깨지는게 가장 이상했다.

4. ORM 을 지원한다.

이건 여러 프레임워크를 다뤄 보면서 느꼈지만 유지보수를 하다보면 결국 select 는 쌩쿼리를 날리고 insert, update, delete 정도 사용한다. 만약 insert, update, delete 조건이 복잡해지면 결국 다시 쌩쿼리를 날리는것도 마찬가지긴해서 자세하게는 확인하지 않았다.

 

php 에서 가장 빠르다고 소개하는 프레임워크인데 프론트 부분에서 htmx 경험이 너무 안좋아서 덮어두고 넣어가기로 했다. 프론트 코드가 유지보수되도록 컴포넌트 형식으로 가지 않는다면 HMVC 라면 더더욱 유지보수에 불편함을 느낀다생각한다.

'일상' 카테고리의 다른 글

요즘 정리 - php sql 펑션만들기  (2) 2024.09.14
내가 생각하는 아키텍처 패턴?  (0) 2024.07.28
HMVC 패턴?  (0) 2024.07.28

 

 

내가 생각하는 아키텍처 패턴을 정리해보고 싶다.

 

내가 개인적으로 느끼는 부분만 정리해보자면

 

mvc, mvc2, mvvm, mvp

이 친구들은 다 비슷비슷하다. 

차이를 구분하라고해도 뭔가 이름만 바뀐거같지 뭔가 구조가 변한건 없어보이기만 한다.

 

HMVC 

이건 mvc 를 패키징해서 한개의 프로젝트에 여러개의 mvc 구조를 사용할 수 있다.

그래서 이론만 따지면 확장하는게 자유롭다.

 

클린 아키텍쳐

뭔가 객체지향 프로그래밍을 기반으로 해서 좀 더 고도화해서 유지보수성을 올린거같이 보이는데 이 아키텍처가 뜨거운 감자처럼 좋다 안좋다 말이 너무 많아서 어느정도 정리되면 공부해볼 생각이다.

 

마이크로서비스

이건 나도 조금 회의적이고 반대의 입장이다.

쉽게 풀어서 생각하면 하나의 모회사 서비스가 자회사를 여럿 두는 구조다.

실제 구조는 모든 프로젝트가 모회사이자 자회사이겠지만 프로젝트가 잘되고 성공해봐야 마이크로서비스를 도입할수있으니 비즈니스 로직에서 모회사 자회사 개념으로 구현된 마이크로서비스가 있을수도 있고 독립적인 모회사이자 자회사가 있을 수 있다. 이 아키텍처패턴을 가지고 진지하게 얘기를 해주신 포프라는 유튜버가 있는데 이분의 말을 듣고 설득되어버렸다. 각 서비스가 독립적인 서비스를 가지는 것은 좋다! 하지만 기술적 분리는 반대다. 그런데 기술적 분리를 가져가지 않고 서비스를 나누면 서비스를 나누는 의미가 좀 퇴색되는 것도 맞긴하다. 

 

 

 

'일상' 카테고리의 다른 글

요즘 정리 - php sql 펑션만들기  (2) 2024.09.14
trongate ? 간단후기  (1) 2024.09.14
HMVC 패턴?  (0) 2024.07.28

 

일을 하다가 

혼자서 여러개의 프로젝트를 유지보수는 기본이고 추가 개발까지 진행을 해보니 느낀게 있다

 

이거 MVC 패턴이 뭔가 잘못된건 아닐까?

 

MVC 패턴이 구조 설계가 문제가 있다는게 아니다.

좀 더 좀 더 확장성있게 변화할 필요가 있었다.

 

여러개의 프로젝트를 동시에 유지보수를 하고 새로운 프로젝트가 추가 되면

위에서는 이렇게 말한다.

"ㅇㅇ씨 새로 추가되는 ㅇㅇ프로젝트가 지금 유지보수 하고있는 ㄴㄴ프로젝트 있죠?
그거랑 비슷하게 가면되요~~"

 

그럼 나는 ㄴㄴ프로젝트를 복사해서 재활용을 준비한다.

하지만 무조건 a부터 z까지 모두 동일한 로직으로 흘러가는 프로젝트는 없다.

중간에 기능이 추가되어 새로 추가된 ㅇㅇ프로젝트에서는 필요가 없는 코드도 정리해야한다.

그러면 아무리 코드를 잘짠다 하더라고 디비 컬럼도 정리하고 한두번씩 테스트는 무조건 당연하다. 

내가 깔끔하게 덜어냈다 하더라도 추후에 문제가 될 여지가 있는지 체크도 반드시 해야한다.

하지만 일하는 환경이 그런거까지 꼼꼼하게 해야하지만 여유롭게 사이드 이펙트를 생각하며 돌아가지 않고 항상 분주하고 정신 사납다.

 

그렇다면 반드시 실수를 하게된다.

꼼꼼하게 하려고 했지만 1, 2, 3, 4 스탭을 진행해서 순차적으로 업무를 해야하지만 정신이 없을때는 일의 순서가

1 -> 가 -> 나 -> A -> B -> 2 -> 가-1 -> 나-2 -> 3  

 

이런식으로 순서가 뒤죽박죽 섞이게 된다. 사람은 실수를 하게 되면 그것을 고치려고 노력을 반드시 해서 점점 나아지는 모습을 보여야 하지만 때로는 정말 내가 무슨일을 하는지 어디까지 일을 했는지 todo를 작성해도 아주 사소한 실수라도 하게된다. 나는 어느순간 이렇게 생각했다. 내가 실수를 줄이는 상황을 만들자!

 

 

프론트 컨포넌트를 빼서 유지보수성을 올린것처럼 백단에서도 패키징을 하면 안될까?

mvc패턴이 한프로젝트에 하나가 아닌 한프로젝트에 여러개의 mvc 패턴이 패키징이 되어서 한번 잘만들어두고 패키징만 잘되면 서로 기능적으로 로직적으로 관계를 느슨하게 만들어서 테스트를 내가 덜 하더라도 에러 확률을 확실하게 낮출 수 있는 방법을 생각해봤다. 

 

이러한 상황을 챗gpt에 물어보고 검색하고 찾아보니 누군가가 나와 똑같은 일을 겪은건지 이미 나와있었다.

왜 나는 몰랐던건지 조금 자책하고 늦게라도 안게 어디냐고 위안도 삼았다.

그리고 조금 더 찾아보고 HMVC 패턴으로 구현된 프레임워크나 좋은 사례가 거의 없다시피 한걸 알게 되었다.

유일하게 php 에서 찾았다.

trongate 라는 이름의 프레임워크인데 이걸 배워서 실무에 적용할 수 있는 만큼 빠르게 적용해보는게 나의 목표이다. 

 

 

 

 

 

 

 

 

'일상' 카테고리의 다른 글

요즘 정리 - php sql 펑션만들기  (2) 2024.09.14
trongate ? 간단후기  (1) 2024.09.14
내가 생각하는 아키텍처 패턴?  (0) 2024.07.28

이번에 플젝을 next 를 쓰는 플젝을 진행했는데 다른건 그냥 만들면 됐는데 처음 가장 발목잡았던 next auth...

설정다하면 편하기는 편한데 설정이 복잡하다...

 

우선 next 디렉토리 구소를 api 형식이다. pages 라면 구글링해보면 다른 예제가 많아서 구글링을 추천한다.

 

https://next-auth.js.org/

 

NextAuth.js

Authentication for Next.js

next-auth.js.org

 

여기서 npm install 해주고 폴더구조를 우선

api/auth/[...nextauth]/route.js 를 정한다. ts 안쓰는 이유는 내가 안쓰는거까지도 타입다 지정해야하고 너무 불편하다

구글 애플/ 네이버 카카오 리턴이 다른데 콜백에서 접근할때 없는거니까 접근안된다고 에러까지 뜬다..

이런 에러 헨들링 가능하면 ts 를 추천한다.

 

 

 

import NextAuth, { AuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import AppleProvider from "next-auth/providers/apple";
import KakaoProvider from "next-auth/providers/kakao";
import NaverProvider from "next-auth/providers/naver";
import CredentialsProvider from "next-auth/providers/credentials";
import { BASE_URL, JWT_SECRET } from "@/util/path";
import jwt from 'jsonwebtoken';

import { SignJWT } from "jose";
import { createPrivateKey, KeyObject } from "crypto";

const authOptions = {

	session: {
		strategy: "jwt",
		maxAge: 60 * 60 * 24 * 7, // 31일
		updateAge: 60 * 60 * 24, // 1일
	},
	cookies: {
		pkceCodeVerifier: {
			name: "next-auth.pkce.code_verifier",
			options: {
				httpOnly: true,
				sameSite: "none",
				path: "/",
				secure: true,
			},
		},
	},
	pages: {
		signIn: "/next/login", // 로그인 페이지
		signOut: "/next/signout", // 로그아웃 페이지
		error: "/next/api/auth/error", // 오류 페이지
		verifyRequest: "/next/api/auth/verify-request", // 이메일 확인 페이지
	},
	providers: [
		CredentialsProvider({
			name: "Credentials",
			credentials: {
				username: { label: "Username", type: "text" },
				password: { label: "Password", type: "password" },
			},
			async authorize(credentials, req) {

				// 로그인 인증 처리 추가해야함
				
				const response_j = await response.json();

				let user = { id: null, name: null, email: null };

				if (response_j?.result) {
					user = { id: credentials.username, name: null, email: response_j._email };
				}

				if (user.id) {
					return user;
				} else {
					return null;
				}
			},
		}),

		GoogleProvider({
			clientId: process.env.GOOGLE_CLIENT_ID,
			clientSecret: process.env.GOOGLE_KEY,
		}),
		AppleProvider({
			clientId: process.env.APPLE_ID,
			clientSecret: await getAppleToken(),
		}),
		KakaoProvider({
			clientId: process.env.KAKAO_API_KEY,
			clientSecret: process.env.KAKAO_SCRIPT_KEY,
		}),
		NaverProvider({
			clientId: process.env.NAVER_API_KEY,
			clientSecret: process.env.NAVER_CLIENT_SECRET,
		}),


	],
	secret: process.env.NEXTAUTH_SECRET,
	callbacks: {
		async signIn({ user, account, profile, email, credentials, context }) {
			console.log("======signIn======");

			if (account.provider === "apple" || account.provider === "naver" || account.provider === "kakao" || account.provider === "google") {
				// 1. 가입 유무 체크  user.id
				
				const response_j = await response.json();

				if (response_j.result) {
					// 3. 가입 되어있으면 로그인 완료
					// console.log("regiCheck > response_j:: ", response_j)
				} else {

					// 2. 가입 안되면 간편가입으로 (필요 데이터 넘기기)
					// 전화번호, 아이디, 이메일, 이름, 닉네임, 소셜로그인,
					let nickname = null
					let phonenumber = null

					if (account.provider === "kakao") {
						nickname = profile.properties.nickname
						phonenumber = profile.kakao_account.phone_number
						if (phonenumber) {
							phonenumber = phonenumber.replace('+82', '0').replace(/[\s-]+/g, "").replace(/[^0-9]*/s, '');
						}
					} else if (account.provider === "naver") {
						nickname = profile.response.nickname
						phonenumber = profile.response.mobile
						if (phonenumber) {
							phonenumber = phonenumber.replace('+82', '0').replace(/[\s-]+/g, "").replace(/[^0-9]*/s, '');
						}
					}
					// 여기서 로그인 안되어있으니까 다른 페이지로 이동					

                }

			}

			if (account.provider === "credentials") {

			}

			return true;
		},
		async jwt({ token, trigger, session, user}) {
			console.log("======jwt======");

			let uToken = null;
			if (token?.uId !== null && token?.uId !== undefined) {
				uToken = jwt.sign({
					username: token.username,
				}, JWT_SECRET, { expiresIn: '5m' });
				token.uToken = uToken;
			}

			if (token?.sub) {
				console.log("======jwt setUserData======");
				

				const response_j = await response.json();

				// console.log("jwt > > response_j: ",response_j)
				if (response_j?.uData) {
					// 여기서 로그인 성공시 추가 테이터 지정 
                    token.username = response_j?.uData?.username;
				}
			}

			return token;
		},
		async session({ session, token, trigger, newSession }) {
			// console.log("======session======");

			// 토큰으로 지정한애들이 token. 안에 다 들어있어서 세션에 추가해야지 우리가 접근할수있다
			if (token.username !== undefined) {
				session.user.username = token.username;
			}

			return session;
		},

	},
};

// 이건 애플로그인 할때 필요한건데 그냥 넣자
async function getAppleToken() {
	const key = await process.env.APPLE_SECRET;

	const privateKey = createPrivateKey({
		key: key,
		format: "pem",
		type: "pkcs8",
	});

	const appleToken = await new SignJWT({})
		.setAudience("https://appleid.apple.com")
		.setIssuer(process.env.APPLE_TEAM_ID)
		.setIssuedAt(Math.floor(Date.now() / 1000))
		.setExpirationTime(Math.floor(Date.now() / 1000) + 3600 * 2)
		.setSubject(process.env.APPLE_ID)
		.setProtectedHeader({
			alg: "ES256",
			kid: process.env.APPLE_KEY_ID,
		})
		.sign(privateKey);

	return appleToken;
}


const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

 

 

이렇게 코드를 크게 짤수있다. 쿠키에 pkceCodeVerifier 이거는 애플로그인할때 필요하다. 중요 코드는 다빼서 알아서 추가해서 쓰자

pages 에 /next 를 다 붙이는 이유는 

 

next config 에 

    basePath: '/next',
    assetPrefix: '/next/',

이걸 추가해서 넣었다 

 

그리고 나처럼 로그인 다되고 user 를 뜯어고치고 싶으면 app/type/next-auth.d.ts 를 추가하자

 

import NextAuth, { DefaultSession } from "next-auth"

declare module "next-auth" {
  /**
   * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
   */
  interface Session {

    // 추가할 세션 항목
    user: {
      username: string | null,
    }
    // & DefaultSession["user"] // 기존 세션 항목 유지
  }
  interface User {

  }
}

 

이렇게 추가하면 된다

 

그리고 api 의 루트 layout 구조도 변경해야하는데

import React from "react";
import "./globals.css";

import { NextAuthProvider } from "./providers";

export default function RootLayout({ children,}: { children: React.ReactNode; }) {
  return (
    <html >
      <body>
        <div className=" bg-white">
          <NextAuthProvider>
            {children}
          </NextAuthProvider>
        </div>
      </body>
    </html>
  );
}


이렇게 수정하고 NextAuthProvider 는 

"use client";

import { SessionProvider } from "next-auth/react";
import React from "react";

type Props = {
  children?: React.ReactNode;
};

export const NextAuthProvider = ({ children }: Props) => {
  return <SessionProvider basePath="/next/api/auth">{children}</SessionProvider>;
};

이렇게 설정하자 

 

env 설정은 다른거는 다 구글링하면서 알아서 하겠지만

NEXTAUTH_SECRET

JWT_SECRET

NEXTAUTH_URL="만약에 나처럼 url 건들었음 https://도메인명/next/api/auth/ 이런식으로 지정해야한다 "  

APPLE_SECRET="-----BEGIN PRIVATE KEY-----\n여기에 키 내용 처음 열었을때 줄바꿈마다\n 을 붙여야함\n-----END PRIVATE KEY-----\n"

이거는 꼭 확인해서 적어주자 

폼검증을 조금 수정했다 여기서 조금 미완성인 부분이 틀렸을때 보여줘야하는 텍스트 대상을 완벽하게 못잡는거다

class FormValidator {
	constructor(form, fields) {
		this.form = form;
		this.fields = fields;
	}

	initialize() {
		this.validateOnEntry(); // 실시간 감지
		// this.validateOnSubmit();
		this.validateOnSubmit2();
	}

	initialize_nullTrue() {
		this.validateOnEntry(true); // 실시간 감지
		// this.validateOnSubmit();
		this.validateOnSubmit2(true);
	}

	validateOnSubmit() {
		let self = this;

		this.form.addEventListener("submit", (e) => {
			e.preventDefault();
			self.fields.forEach((field) => {
				const input = document.getElementsByName(`${field}`);

				if (input[0].type == "checkbox") {
					self.validateFields_Checkbox(input[0], field);
				} else if (input[0].type == "select-one") {
					self.validateFields_Selectbox(input[0]);
				} else {
					self.validateFields(input);
				}
			});
		});
	}

	validateOnSubmit2(isnull = false) {
		// e.preventDefault();
		let self = this;
		self.fields.forEach((field) => {
			const input = document.getElementsByName(`${field}`);

			if (input[0]) {
				if (input[0].type == "checkbox") {
					self.validateFields_Checkbox(input[0], field, isnull);
				} else if (input[0].type == "select-one") {
					self.validateFields_Selectbox(input[0], isnull);
				} else {
					self.validateFields(input, isnull);
				}
			}
		});
	}

	validateOnEntry(isnull = false) {
		let self = this;
		this.fields.forEach((field) => {
			const input = document.getElementsByName(`${field}`);

			if (input[0]) {
				if (input[0].type == "checkbox") {
					input[0].addEventListener("input", (event) => {
						self.validateFields_Checkbox(input[0], field, isnull);
					});
				} else if (input[0].type == "select-one") {
					input[0].addEventListener("input", (event) => {
						self.validateFields_Selectbox(input[0], isnull);
					});
				} else {
					input[0].addEventListener("input", (event) => {
						self.validateFields(input, isnull);
					});
				}
			}
		});
	}

	validateFields(fields, isnull = false) {
		let formval_min = fields[0].getAttribute("formval_min");
		let formval_max = fields[0].getAttribute("formval_max");

		fields.forEach((field) => {
			if (field.value.trim() === "" && !isnull) {
				this.setStatus(field, `${field.parentElement.parentElement.querySelector("label").innerText} 을/를 입력해주세요`, "error");
			} else {
				if (field.value.trim() !== "") {
					this.setStatus(field, null, "success");
				}

				if (field.type === "text") {
					if (formval_max && formval_min) {
						if (formval_max < field.value.trim().length || formval_min > field.value.trim().length) {
							this.setStatus(field, formval_min + "자 이상 " + formval_max + "자 이하로 입력해주세요.", "error");
						}
					} else {
						if (formval_min) {
							if (formval_min > field.value.trim().length) {
								this.setStatus(field, formval_min + "자 이상 입력해주세요.", "error");
							}
						}
						if (formval_max) {
							if (formval_max < field.value.trim().length) {
								this.setStatus(field, formval_max + "자 이하로 입력해주세요.", "error");
							}
						}
					}
					let formcnt_min = fields[0].getAttribute("formcnt_min");
					let formcnt_max = fields[0].getAttribute("formcnt_max");

					let extractedNumber_Input = parseInt(field.value.trim().replace(/[^0-9.-]+/g, ""));

					let extractedNumber_Min
					let extractedNumber_Max
					if (formcnt_min) {
						extractedNumber_Min = parseInt(formcnt_min.replace(/[^0-9.-]+/g, ""));
					}
					if (formcnt_max) {
						extractedNumber_Max = parseInt(formcnt_max.replace(/[^0-9.-]+/g, ""));
					}

					if (formcnt_min && formcnt_max) {
						if (extractedNumber_Max < extractedNumber_Input || extractedNumber_Min > extractedNumber_Input) {
							this.setStatus(field, formcnt_min + " 이상 " + formcnt_max + " 이하로 입력해주세요.", "error");
						}
					} else {
						if (formcnt_min) {
							if (extractedNumber_Min > extractedNumber_Input) {
								this.setStatus(field, formcnt_min + " 이상 입력해주세요.", "error");
							}
						}
						if (formcnt_max) {
							if (extractedNumber_Max < extractedNumber_Input) {
								this.setStatus(field, formcnt_max + " 이하로 입력해주세요.", "error");
							}
						}
					}

				}

				if (field.type === "email") {
					const re = /\S+@\S+\.\S+/;
					if (re.test(field.value)) {
						this.setStatus(field, null, "success");
					} else {
						this.setStatus(field, "이메일 형식이 맞지 않습니다.", "error");
					}
				}

				if (field.id === "password_confirmation") {
					const passwordField = this.form.querySelector("#password");

					if (field.value.trim() == "") {
						this.setStatus(field, "비밀번호를 입력해주세요.", "error");
					} else if (field.value != passwordField.value) {
						this.setStatus(field, "비밀번호 두개가 일치하지 않습니다.", "error");
					} else {
						this.setStatus(field, null, "success");
					}
				}
			}
		});
	}

	validateFields_Checkbox(field, checkboxName, isnull = false) {
		const selectedCheckboxLength = document.querySelectorAll(`input[name="${checkboxName}"]:checked`).length;
		let formval_min = field.getAttribute("formval_min");
		let formval_max = field.getAttribute("formval_max");

		if (selectedCheckboxLength == 0 && !isnull) {
			this.setStatus(field, `${field.parentElement.parentElement.querySelector("label").innerText} 을/를 선택해주세요`, "error");
		} else {
			this.setStatus(field, null, "success");
		}

		if (formval_max && formval_min) {
			if (formval_max < selectedCheckboxLength || formval_min > selectedCheckboxLength) {
				this.setStatus(field, formval_min + "개 이상 " + formval_max + "개 이하로 선택해주세요.", "error");
			}
		} else {
			if (formval_min) {
				if (formval_min > selectedCheckboxLength) {
					this.setStatus(field, formval_min + "개 이상 선택해주세요.", "error");
				}
			}
			if (formval_max) {
				if (formval_max < selectedCheckboxLength) {
					this.setStatus(field, formval_max + "개 이하로 선택해주세요.", "error");
				}
			}
		}
	}

	validateFields_Selectbox(field, isnull = false) {
		if (field.value.trim() === "" && !isnull) {
			this.setStatus(field, `${field.previousElementSibling.previousElementSibling.innerText} 을/를 선택해주세요`, "error");
		} else {
			this.setStatus(field, null, "success");
		}
	}

	setStatus(field, message, status) {
		const successIcon = field.parentElement.querySelector(".iconSuccess") ? field.parentElement.querySelector(".iconSuccess") : field.parentElement.parentElement.querySelector(".iconSuccess");
		const errorIcon = field.parentElement.querySelector(".iconError") ? field.parentElement.querySelector(".iconError") : field.parentElement.parentElement.querySelector(".iconError");
		const errorMessage = field.parentElement.querySelector(".errorMessage") ?
			field.parentElement.querySelector(".errorMessage") :
			field.parentElement.parentElement.querySelector(".errorMessage");

		if (status === "success") {
			if (errorIcon) {
				errorIcon.classList.add("hidden");
			}
			if (errorMessage) {
				errorMessage.innerText = "";
			}
			if (successIcon) {
				successIcon.classList.remove("hidden");
			}
			field.classList.remove("input-error");
		}

		if (status === "error") {
			if (successIcon) {
				successIcon.classList.add("hidden");
			}
			errorMessage.innerText = message;
			errorIcon.classList.remove("hidden");
			field.classList.add("input-error");
		}
	}
}

그냥 폼검증말고 왜 틀렸는지 이런걸 알려주는 폼검증을 만들어보자!

js 라이브러리 등등 이런게 많지만 내입맛에 맞게 수정하기 편하기 위해서는 결국 직접 만드는것이 가장 좋겠다고 판단했다
https://codepen.io/trending

 

CodePen

An online code editor, learning environment, and community for front-end web development using HTML, CSS and JavaScript code snippets, projects, and web applications.

codepen.io

여기 사이트에서 적당한 예시를 찾아서 수정했는데 정확한 url을 다시 찾으려고하니 못찾겠다...

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Document</title>
		<style>
			* {
				box-sizing: border-box;
			}

			body {
				background-color: blueviolet;
			}

			.title {
				margin-bottom: 2rem;
			}

			.hidden {
				display: none;
			}

			.icon {
				width: 24px;
				height: 24px;
				position: absolute;
				top: 32px;
				right: 5px;
				pointer-events: none;
				z-index: 2;

				&.icon-success {
					fill: green;
				}

				&.icon-error {
					fill: red;
				}
			}

			.container {
				max-width: 460px;
				margin: 3rem auto;
				padding: 3rem;
				border: 1px solid #ddd;
				border-radius: 0.25rem;
				background-color: white;
				box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
			}

			.label {
				font-weight: bold;
				display: block;
				color: #333;
				margin-bottom: 0.25rem;
				color: #2d3748;
			}

			.input {
				appearance: none;
				display: block;
				width: 100%;
				color: #2d3748;
				border: 1px solid #cbd5e0;
				line-height: 1.25;
				background-color: white;
				padding: 0.65rem 0.75rem;
				border-radius: 0.25rem;
				box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06);

				&::placeholder {
					color: #a0aec0;
				}

				&.input-error {
					border: 1px solid red;

					&:focus {
						border: 1px solid red;
					}
				}

				&:focus {
					outline: none;
					border: 1px solid #a0aec0;
					box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
					background-clip: padding-box;
				}
			}

			.input-group {
				margin-bottom: 2rem;
				position: relative;
			}

			.error-message {
				font-size: 0.85rem;
				color: red;
        display: block;
			}

			.button {
				background-color: blueviolet;
				padding: 1rem 2rem;
				border: none;
				border-radius: 0.25rem;
				color: white;
				font-weight: bold;
				display: block;
				width: 100%;
				text-align: center;
				cursor: pointer;

				&:hover {
					filter: brightness(110%);
				}
			}

			.promo {
				color: white;
				opacity: 0.75;
				margin: 1rem auto;
				max-width: 460px;
				background: rgba(255, 255, 255, 0.2);
				padding: 20px;
				border-radius: 0.25rem;

				a {
					color: white;
				}
			}
		</style>
	</head>
	<body>
		<div class="container">
			<h2 class="title">Create a new account</h2>
			<form action="#" class="form">
				<div class="input-group">
					<label for="username" class="label">이름</label>
					<input id="username" placeholder="webcrunch" type="text" class="input" name="username" formval_min="5" formval_max="15" />

					<span class="error-message"></span>
					<svg class="icon icon-success hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg class="icon icon-error hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
					</svg>
				</div>

				<div class="input-group">
					<label for="username" class="label">셀렉트 박스</label>
					<span class="error-message"></span>

					<select name="selectBox0" id="selectBox0">
						<option value="">선택x</option>
						<option value="1">선택1</option>
						<option value="2">선택2</option>
						<option value="3">선택3</option>
						<option value="4">선택4</option>
					</select>

					<svg class="icon icon-success hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg class="icon icon-error hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
					</svg>
				</div>

				<div class="input-group">
					<label class="label">체크박스</label>
					<span class="error-message"></span>

					<label for="checkbox1" class="label">체크박스선택1</label>
					<input id="checkbox1" name="checkbox00" type="checkbox" class="formValCheckBox" value="1" formval_min="1" formval_max="2" />
					<label for="checkbox2" class="label">체크박스선택2</label>
					<input id="checkbox2" name="checkbox00" type="checkbox" class="formValCheckBox" value="2" />
					<label for="checkbox2-1" class="label">체크박스선택2.1</label>
					<input id="checkbox2-1" name="checkbox00" type="checkbox" class="formValCheckBox" value="2.1" />

					<svg class="icon icon-success hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg class="icon icon-error hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
					</svg>
				</div>

				<div class="input-group">
					<label class="label">체크박스</label>
					<span class="error-message"></span>

					<div>
						<label for="checkbox3" class="label">체크박스선택3</label>
						<input id="checkbox3" name="checkbox01" type="checkbox" class="" value="3" />
					</div>
					<div>
						<label for="checkbox4" class="label">체크박스선택4</label>
						<input id="checkbox4" name="checkbox01" type="checkbox" class="" value="4" />
					</div>

					<svg class="icon icon-success hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg class="icon icon-error hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
					</svg>
				</div>

				<div class="input-group">
					<label for="email" class="label">Email</label>
					<input id="email" type="email" class="input" name="email" autocomplete placeholder="andy@web-crunch.com" />

					<span class="error-message"></span>
					<svg class="icon icon-success hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg class="icon icon-error hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
					</svg>
				</div>

				<div class="input-group">
					<label for="password" class="label">Password</label>
					<input id="password" type="password" class="input" name="password" />

					<span class="error-message"></span>
					<svg class="icon icon-success hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg class="icon icon-error hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
					</svg>
				</div>

				<div class="input-group">
					<label for="password_confirmation" class="label">Password Confirmation</label>
					<input id="password_confirmation" type="password" class="input" name="password_confirmation" />

					<span class="error-message"></span>
					<svg class="icon icon-success hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
					</svg>
					<svg class="icon icon-error hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
						<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
					</svg>
				</div>

				<input type="submit" class="button" value="Create account" />
			</form>
		</div>

	</body>

	<script>
		class FormValidator {
			constructor(form, fields) {
				this.form = form;
				this.fields = fields;
			}

			initialize() {
				// this.validateOnEntry(); // 실시간 감지
				this.validateOnSubmit();
			}

			validateOnSubmit() {
				let self = this;

				this.form.addEventListener("submit", (e) => {
					e.preventDefault();
					self.fields.forEach((field) => {
						const input = document.getElementsByName(`${field}`);

						if (input[0].type == "checkbox") {
							self.validateFields_Checkbox(input[0], field);
						} else if (input[0].type == "select-one") {
							self.validateFields_Selectbox(input[0]);
						} else {
							self.validateFields(input);
						}
					});
				});
			}

			validateOnSubmit2() {
				// e.preventDefault();
				let self = this;
				self.fields.forEach((field) => {
					const input = document.getElementsByName(`${field}`);
					self.validateFields(input);
					if (input[0].type == "checkbox") {
						self.validateFields_Checkbox(input[0], field);
					} else if (input[0].type == "select-one") {
						self.validateFields_Selectbox(input[0]);
					} else {
						self.validateFields(input);
					}
				});
			}

			validateOnEntry() {
				let self = this;
				this.fields.forEach((field) => {
					const input = document.getElementsByName(`${field}`);

					input.addEventListener("input", (event) => {
						self.validateFields(input);
					});
				});
			}

			validateFields(fields) {
				let formval_min = fields[0].getAttribute("formval_min");
				let formval_max = fields[0].getAttribute("formval_max");

				fields.forEach((field) => {
					if (field.value.trim() === "") {
						this.setStatus(field, `${field.previousElementSibling.innerText} 을/를 입력해주세요`, "error");
					} else {
						if (field.value.trim() !== "") {
							this.setStatus(field, null, "success");
						}

						if (field.type === "text") {
							if (formval_max && formval_min) {
								if (formval_max < field.value.trim().length || formval_min > field.value.trim().length) {
									this.setStatus(field, formval_min + "자 이상 " + formval_max + "자 이하로 입력해주세요.", "error");
								}
							} else {
								if (formval_min) {
									if (formval_min > field.value.trim().length) {
										this.setStatus(field, formval_min + "자 이상 입력해주세요.", "error");
									}
								}
								if (formval_max) {
									if (formval_max < field.value.trim().length) {
										this.setStatus(field, formval_max + "자 이하로 입력해주세요.", "error");
									}
								}
							}
						}

						if (field.type === "email") {
							const re = /\S+@\S+\.\S+/;
							if (re.test(field.value)) {
								this.setStatus(field, null, "success");
							} else {
								this.setStatus(field, "이메일 형식이 맞지 않습니다.", "error");
							}
						}

						if (field.id === "password_confirmation") {
							const passwordField = this.form.querySelector("#password");

							if (field.value.trim() == "") {
								this.setStatus(field, "비밀번호를 입력해주세요.", "error");
							} else if (field.value != passwordField.value) {
								this.setStatus(field, "비밀번호 두개가 일치하지 않습니다.", "error");
							} else {
								this.setStatus(field, null, "success");
							}
						}
					}
				});
			}

			validateFields_Checkbox(field, checkboxName) {
				const selectedCheckboxLength = document.querySelectorAll(`input[name="${checkboxName}"]:checked`).length;
				let formval_min = field.getAttribute("formval_min");
				let formval_max = field.getAttribute("formval_max");

				if (selectedCheckboxLength == 0) {
					this.setStatus(field, `${field.parentElement.parentElement.querySelector("label").innerText} 을/를 선택해주세요`, "error");
				} else {
					this.setStatus(field, null, "success");
				}

				if (formval_max && formval_min) {
					if (formval_max < selectedCheckboxLength || formval_min > selectedCheckboxLength) {
						this.setStatus(field, formval_min + "개 이상 " + formval_max + "개 이하로 선택해주세요.", "error");
					}
				} else {
					if (formval_min) {
						if (formval_min > selectedCheckboxLength) {
							this.setStatus(field, formval_min + "개 이상 선택해주세요.", "error");
						}
					}
					if (formval_max) {
						if (formval_max < selectedCheckboxLength) {
							this.setStatus(field, formval_max + "개 이하로 선택해주세요.", "error");
						}
					}
				}
			}

			validateFields_Selectbox(field) {
				if (field.value.trim() === "") {
					this.setStatus(field, `${field.previousElementSibling.previousElementSibling.innerText} 을/를 선택해주세요`, "error");
				} else {
          this.setStatus(field, null, "success");
        }
			}

			setStatus(field, message, status) {
				const successIcon = field.parentElement.querySelector(".icon-success") ? field.parentElement.querySelector(".icon-success") : field.parentElement.parentElement.querySelector(".icon-success");
				const errorIcon = field.parentElement.querySelector(".icon-error") ? field.parentElement.querySelector(".icon-error") : field.parentElement.parentElement.querySelector(".icon-error");
				const errorMessage = field.parentElement.querySelector(".error-message")
					? field.parentElement.querySelector(".error-message")
					: field.parentElement.parentElement.querySelector(".error-message");

				if (status === "success") {
					if (errorIcon) {
						errorIcon.classList.add("hidden");
					}
					if (errorMessage) {
						errorMessage.innerText = "";
					}
					successIcon.classList.remove("hidden");
					field.classList.remove("input-error");
				}

				if (status === "error") {
					if (successIcon) {
						successIcon.classList.add("hidden");
					}
					errorMessage.innerText = message;
					errorIcon.classList.remove("hidden");
					field.classList.add("input-error");
				}
			}
		}

		const form = document.querySelector(".form");

		// name 명
		const fields = ["username", "email", "password", "password_confirmation", "checkbox00", "checkbox01", "selectBox0"];

		const validator = new FormValidator(form, fields);
		validator.initialize();
	</script>
</html>

이렇게 만들었는데 여기에서 입맛에 맞게 추가수정을 하면 될거같다

애초에 처음 프로젝트 시작을 리액트로 하거나 next.js 로 하거나

vue.js 로 하거나 nuxt.js 로 했더라면

이 글을 읽을 필요가 없다

이건 이미 spa가 나오기 이전에 이미 프로젝트를 만들어두고 유지보수를 하다가

디자이너 또는 클라이언트의 요청때문에 spa의 기능을 넣어야할때 조금 편하게 유지보수를 도와줄 수 있도록 알려줄 수 있다

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>

  <style>
  .display-none {
    display: none;
  }
  </style>
</head>

<body>
  <div class="display-none mx-auto" id="app">
    <div v-for="item in items" :key="item">{{ item }}</div>
    <div style="height: 400px; overflow-y: scroll;">
      Scroll down to load more items
      <div style="height: 800px; overflow-y: scroll;">

      </div>
    </div>
  </div>

</body>

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>

<script>
const {
  createApp,
  ref,
  onMounted
} = Vue;

// vue 요소 보이는 부분 해소
$(document).ready(function() {
  $("#app").removeClass("display-none");
});

createApp({
  setup() {
    const items = ref(['Item 1', 'Item 2', 'Item 3']);

    const handleScroll = () => {

      // // 특정요소에 걸때
      // const scrollTarget = document.getElementById('scrollTarget');
      // if (scrollTarget.scrollTop + scrollTarget.clientHeight + 100 >= scrollTarget.scrollHeight) {
      //   const newItem = `Item ${items.value.length + 1}`;
      //   items.value.push(newItem);
      // }

      // 바디 전체
      const scrollTarget = document.documentElement;
      if (scrollTarget.scrollTop + window.innerHeight >= scrollTarget.scrollHeight) {
        const newItem = `Item ${items.value.length + 1}`;
        items.value.push(newItem);
      }
    };

    onMounted(() => {

      // // 특정요소에 걸때
      // const scrollTarget = document.getElementById('scrollTarget');
      // scrollTarget.addEventListener('scroll', handleScroll);

      // 바디 전체
      window.addEventListener('scroll', handleScroll);
    });

    return {
      items,
      handleScroll
    };
  }
}).mount('#app');
</script>

</html>

 이것처럼 vue의 기능중에 리스트 관련 상태관리를 할 수 있어서 이걸 응용하면 다양하게 가능하다 버튼을 클릭했을때 다른 리스트를 불러오는것도 가능해지고 한 페이지 내에서 vue의 기능중에 일부분을 필요한만큼 사용할 수 있다

사실 리스트만 상태관리가 가능해져도 훨씬 많은 코드가 줄어든다

이렇게 하기전에는 이렇게 했다

1. 리스트를 컨트롤러에서 기본으로 로드한다.

2. 리스트 변화가 있으면 ajax 또는 fetch 로 데이터를 가져온다.

3. 가져온 데이터를 js로 html을 만들어서 jqery로 aoppend 한다. (중복 리스트 코드 발생)

 

이런식으로 개발했기 때문에 리스트 ui 또는 로직이 변경되면 컨트롤러에서 기본 리스트와 비동기로 가져와서 처리하는 부분 모두 수정을해야해서 유지보수에 아주 불편했다

https://nuxt.com/docs/getting-started/installation

 

Installation · Get Started with Nuxt

Get started with Nuxt quickly with our online starters or start locally with your terminal. You can start playing with Nuxt 3 in your browser using our online sandboxes: Play on StackBlitzPlay on CodeSandbox Start with one of our starters and themes direct

nuxt.com

 

회사에서 넉스트를 사용하기로해서 넉스트를 공부하는데 처음 배울만한 자료들이 별로 없고 유튜브강의를 보면서 맨땅에 해딩하듯이 공부를 하는데 혹시나 넉스트를 사용하거나 넉스트를 배워 보고자하는 분들을 위해서 정리한다 

 

https://www.youtube.com/watch?v=GBdO5myZNsQ&list=PL4cUxeGkcC9haQlqdCQyYmL_27TesCGPC&index=1 

 

추천할만한 강의 코스로는 이걸 추천한다 여기에서 어느정도 기본적인 부분들을 짚어주는데

이정도만해도 처음에 큰도움이 되었다 

 

 

우선은  nuxt의 구조부터 보자면

 

pages - 라우터

pages 폴더를 만들어서 그안에 vue파일을 만들면 자동으로

/페이지명

이런식으로 라우터를 만들어준다

그리고 조금 신기했던게 폴더명을 [] 대문자에 받을 변수 명을 입력해서 [변수명].vue 를 써서 파일을 생성하면 라우터가

폴더명/변수명

이런식으로 만들어지고 변수를 해당페이지에서 받아올 수 있다.

그럼 여기서 한가지 의문인게 받아야하는 변수가 1개 이상일땐 어떻게 하면될까?

이부분은 내가 여러 테스트와 구글링을 한 결과인데

우선은 변수명이 겹치지 않는 폴더를 하나 만든다 폴더 형식은 [변수명2] 이런식으로 만든다

그리고 그안에 [변수명3].vue 라는 파일을 만든다 이렇게 사용하면 변수명3 페이지는 

폴더명/변수명2/변수명3

이라는 라우터를 가진다 앞에 폴더명이 같을때는 변수가 1개와 2개일때 완전 다른 페이지를 안내하기 때문에 이점을 주의해주어야한다

 

layouts 

layouts 폴더를 만들게 되면 페이지별 일괄적으로 공통으로 들어가는 요소들을 지정할수있다 예를들면 네브바나 푸터가있다. 이게 하나만 사용할수있는게 아니라 그때그때 조금씩 다르게 지정도 가능하기때문에 유연하게 사용이 가능하지만 페이지의 통일성과 사용자 경험을 고려했을때 전체 페이지를 통틀어서 관통하는 공통 요소는 페이지별로 나누려고 손대지 않는 것이 좋다

 

기본적으로 layouts 폴더에 default.vue 를 생성하면 자동으로 공통요소로 인식해서

별 다른 설정이 없어도 자동으로 추가해준다

<template>
    <div>
        <header class="shadow-sm bg-white">
            <nav class="container mx-auto p-4 flex justify-between">
                <NuxtLink to="/" class="font-bold">홈페이지</NuxtLink>
                <ul class="flex gap-4">
                    <li><NuxtLink to="/">홈</NuxtLink></li>
                </ul>
            </nav>
        </header>

        <div class="container mx-auto p-4">
            <slot />
        </div>
        
        <footer class="container mx-auto p-4 flex justify-between border-t-2">
            <ul class="flex gap-4">
                <li><NuxtLink to="/">홈페이지</NuxtLink></li>
            </ul>
        </footer>

    </div>
</template>

<style scoped>
    .router-link-exact-active {
        color: aquamarine;
    }
</style>

기본 형식은 이런데 slot태그에 페이지별 요소들이 들어간다 여기에 욕심내서 slot을 하나이상 사용하는 방법을 찾아보다가 말았는데 slot이 두개 이상 필요한 일을 되도록이면 안만드는 것이 layouts를 잘 사용하는 방법인거같다

그리고 다른 페이지에서는 다른 layouts를 사용하고 싶을땐 다음과 같이 사용하자

<template>
    <div>

    </div>
</template>

<script setup>

definePageMeta({
    layout: '사용하고자하는 layouts 파일명',
});

</script>

<style scoped>

</style>

이렇게 사용하면 자동으로 알아서 다른 layouts를 끼워준다

 

server

이걸 직접 써본적이 없고 처음 써봐서 생소하지만 엄청 강력한 기능같다 예제에는 그냥 api url 을 만들어주는정도 였는데 데이터를 받아서 가공하는 작업중에 반복되는 작업들을 선언해서 리턴해주는 작업도 시킬수있는 등 다양하게 사용이 가능할거같다.

우선 예제를 보면서 따라하기를 권장하지만 예제가 3개월 전인데도 문법이 안맞다... 그래서 지금 시점기준으로 잘동작하는 코드를 공유하자면 이렇다

export default defineEventHandler(async (event) => {

    const { name } = getQuery(event);
    const { aaa } = getQuery(event);

    const { age } = await readBody(event);

    return {
        message: `테스트 ${name}, 하나더 ${age}`
    }

    // getQuery, readBody 이부분에서 문법이 변경되었다 
    // getQuery는 쿼리스트링으로 요청오는거고 readBody는 post요청으로 오는걸 받아볼 수 있다
})

 

 


// 스크롤 멈춤 감지
$.fn.scrollStopped = function (callback) {
    let _this = this, $this = $(_this);
    $this.scroll(function(event) {
        clearTimeout($this.data('scrollTimeout'));
        $this.data('scrollTimeout', setTimeout(callback.bind(_this), 250, event));
    });
};
addEventListener("scroll", e => {
    $(window).scrollStopped(function (ev) {
        // console.log("멈춤")
    });
})

 

let lastScrollY = 0;
addEventListener("scroll", e => {
                const scrollY = window.scrollY;
                // 스크롤 올렸을때, 내렸을때
                if (scrollY < lastScrollY) {
       
                } else {
 
                }
                // 현재의 스크롤 값을 저장
                lastScrollY = scrollY;
})

 

이미 개발이 완료된 페이지를 모달로 불러와야하는 상황에서

iframe으로 불러온다면 어떨까?

 

라는 생각에 시작되었다.

 

구글링을 엄청해서 이렇게 해보라는 수많은 방식을 사용해봤는데 이게 하나도 안먹혔다...

그래도 구글링은 계속했고 여기에서 답을  찾았다

https://jsfiddle.net/zzznara/whL591q2/4/

 

[자바스크립트] javascript로 iframe에 동적으로 소스를 추가하거나 변경하는 방법 - JSFiddle - Code Playgr

 

jsfiddle.net

 

여기에서 보여주는 예시처럼

    // iframe을 담는 변수
    let iframe = document.getElementById('iframe_id');

    // 브라우저 버전에 따라 iframe의 document를 가져온다.
    let iframedoc = iframe.document;
    if (iframe.contentDocument) {
        iframedoc = iframe.contentDocument;
    } else if (iframe.contentWindow) {
        iframedoc = iframe.contentWindow.document;
    }

    let mainPageHeader = iframedoc.querySelector('#mainPageHeader')
    mainPageHeader.style.display = "none";
    let header_area = iframedoc.querySelector('.header-area')
    // header_area.style.display = "none";
    let htmlTag = `
        <header class="">
            <h1 class="" style="padding-bottom:0;">모달이름</h1>
            <a class="modal-close">×</a>
        </header>`
    $(header_area).append(htmlTag);
 
이런식으로 해서 구현했다 

 

작년에는 회고가 없었지만 올해는 생각보다 정리할거리가 있어서 회고를 해보고자 한다

 

 

    2021년 처음 코딩을 접했다. 사실 처음 접했던건 2학년 교양과목이었다. 이때 접했던 코딩은 아주 매운맛이었다. AOS 강의였는데 자바 문법을 안알려줬다. 이거면 얼마나 매운맛이었는지 짐작할 수 있다고 본다.... 교수님은 신나서 타이핑하시는데 수강생들은 열심히 따라치기 바쁘거나 포기하는 모습을 보고는 이런게 코딩이구나... 그러다 점차 문법들을 이해하고 스스로 최소한의 코드를 짜내고 결과물을 만들어 냈다.

 

그러다

21년에 국비로 코딩을 접하고

개발을 배웠다.

 

국비가 끝나고 공부를 하다 취업도하게 되었다.

 

그렇게 메인 기술스텍이 PHP가 되었다.

 

    언어에 좋고 나쁘다 편견은 없지만 메인 기술스텍이 php인 점에서 약점 또한 될 수 있다고 짐작했다.

하지만 큰 신경을 쓰지는 않았다. 딱 하나 언어에 가리는게 있다면 내가 하고자 하는 코드에 부수적으로 달려야하는 코드들이 많은 언어는 안좋아한다. 이게 영어지문도 아니고 읽어야하는 특정 구간만 있는것도 가독성을 해치기 때문에 별로다.

 

    우선 정리하기에 앞서 개발상황이 조금 변해왔던거같다.

    이전에는 프레임워크라고 하면 풀 프레임워크 백, 프론트 둘다 지원되는 프레임워크 였다면 이제는 프론트와 백이 분리된 모습을 보이고 있다. 그렇다면 이런 모습도 가능해진다. 서비스별로 백엔드 언어와 프레임워크를 다르게 줄 수 도 있고, 기능별로 쪼갤 수 도 있고, 페이지별로 쪼갤 수 도 있다.

이게 아무래도 완전완전 큰 글로벌 대기업? 정도의 서비스 규모가 되면 정말 다양한 개발자가 모이게 되고 그런 개발자들은 원하는 기술을 발휘하기 위해 각각 다른 언어를 사용하고 그 다양한 언어를 지원하기 위해서 이렇게 쪼갠건 아닌가 생각이 든다. 

    백엔드만 말했지만 프론트도 얼마든지 가능하다. 각각의 서비스 별 프레임워크를 다르게 가져갈수있다. 한가지 다른점은 페이지별로 프레임워크를 쪼개는거는 안된다. 

백은 API 주소를 만들어서 보낼 데이터만 만들면되고

프론트는 API주소를 요청해서 받은 데이터를 뿌려주기만 하면 된다. 하지만 서버 자원낭비 방지를위해 한번 받고 다시 쓸거같은 데이터는 프론트에서 따로 보관해야하고 관리해야한다.  

    추가로 이게 다시 성능상의 이유로 풀프레임워크로 돌아가는 분위기가 다시 보인다. 하지만 내 생각에는 이게 어느정도는 쪼개질것으로 보인다. 이유는 한번 쪼개진 기능들이 다양한 언어로 구현이 되어있다면 한가지언어로 합치거나 하나의프레임 워크로 합치기 위해서는 올라운드로 지원이 되는 프레임워크가 등장해야한다. 

 

    위에서 말한거처럼 약점 또한 될수도 있다는 것에서 나는 다양한 언어와 프레임 워크를 배워보기로 했다.

국비가 끝나고 공부를 하는 동안 자바 스프링 부트를 공부해봤으니 패스.

 

백엔드

    js의 익스프레스 - 이건 조금 다뤘는데 뭔가 취향이 아니었다.

    파이썬의 장고 - 확실히 코드도 많이 생략되었지만 생략된만큼 상상력으로 코딩하는 기분이나는 장고매직..

    파이썬의 FastAPI - 이건 성능이 빠르고 다 좋았지만 인지도가 낮다는게 아쉬웠다.

 

프론트

    리액트 - 기본 문법부터 html 스럽지 않았다. html을 리턴하는것도 이해하는데 어려웠고 input 이나 html 컨트롤을 위해서 따로 핸들러를 달아줘야하는거에서 일단 멘붕했다. 그리고 상태관리도 많이 어려웠다. 예제로는 리덕스를 사용했는데 파일을 여러개를 왔다갔다해야하고 엄청 어렵게 느껴졌다. 변화가 일어날때마다 알려줘야했던걸로 기억하고 라이브러리마다 자동으로 변화를 감지하는 것도 있다는데 확실히 가볍게 많이 배우려다보니 스킵한 것도 있다.

    뷰 - 리액트를 보다가 뷰를 보니 선녀였다. 뷰는 초기에 점진적 적용을 목적으로 개발된거다보니 원하는 페이지에서만 뷰를 사용하고 빼고 할 수 있다는거에서 자유로웠다.

    스벨트 - 스벨트는 처음 접했을때는 엄청 좋긴했다. 하지만 스벨트에서 권장하는 몇가지 규칙들을 어느정도 정하고나니 뭔가가 어색했다. html스럽긴 하지만 한가지 아쉬운게 뷰처럼 기존에 구현된 html 코드에 녹아들어갈 수 있도록 지원이 안되는거같아서 아쉬웠다. 하지만 스벨트에서 페이지 별 라우터를 볼 수 있게 지원해줘서 테스트 하기도 편했다.

 

    여기서 앱은 어떨까라는 생각에 앱도 관심이 생겨서 플러터를 조금 배웠다. 플러터에서 사용하는? 상태관리 라이브러리가 몇개있는데 그중에 Provider와 GetX를 봤는데 확실히 이건 리액트스러웠다. 변화를 추적하기 위해 여러 사전 작업을 하고 최종적으로 변수를 담았을때 변수가 변했다는걸 알려야한다. 뭔가 좀 더 쉽고 간단한 방법이 언젠가는 나올거같다는 판단으로 플러터는 지켜보기로 했다.

 

 

    이처럼 여러 프레임워크를 사용해보고 경험해보면서 느낀것은 다들 비슷한 동작을 하는데 어쩜이리 다 다르게 굴러가는건가 라는 생각이 들었다. 프론트에서 리액트가 나오고 다들 다양한 방향이 나오게 된건 리액트에서 변화된 요소만! 새로고침 한다는거에서 가장 큰 변화를 불러일으켰다. 웹에서 실시간 갱신 또는 일반적인 웹에서는 스크롤 유지에 주로 사용되는기능이다. 덕분에 위에서 말한부분말고 다양한 부분들이 비동기화되면서 웹이 앱과 비슷한 사용자경험을 주게되었다.

 

 


앞으로의 계획과 생각

앞으로의 웹과 앱은 어떻게 될지는 사실 잘 모르겠다. 

    플러터에서 하나의 코드로 AOS와 IOS를 둘다 지원이 가능하고 PC 웹과 PC 프로그램까지 지원을 목표로 한다는데 여기서 궁금한거는 이거다.

그럼 HTML 코드를 코틀린, 스위프트로 변경해서 빌드하면 되겠네?

그렇다면 역으로도 앱으로 짠 코드를 여러 방식으로 변경해서 빌드하면 되겠네?

 

끝으로

프레임워크들을 여러가지를 다뤄보면서 너무나도 피곤해졌다. 시간도 많이 썼다. 다들 본인의 프레임워크가 좋다길래 얕게나마 써봤다.

    사실 언어와 방식만 다르지 차이가 거의 없다. 각 프레임워크별 지원하는 기능이 당장은 다른게 있더라도

시간이 지나면 분명 지원되거나 다른식으로 풀어나간다. 

    근본적으로 다가간다면 나의 생각은 백에서는 서버 자원 많이 안쓰고 성능 좋은게 장땡이다. 

이걸 코드로 잘 풀어나가는 것 또한 중요하다. 프론트는 백에서 했던말 또 안하게 잘 관리하는게 중요하다. 

 

내년에는 프레임워크 공부는 어느정도 마무리하고

개발에 대해 더 깊게 배우는 것을 목표로 가지고 가야겠다

서버라던지, 보안이라던지, 어떻게 협업을 잘할 수 있는지 이런 부분 공부를 해야겠다.

 

 

 

 

 

+ Recent posts