반응형

input을 사용하여 같은 파일 연속으로 올리기

input을 사용해서 파일을 올릴때 연속으로 같은 파일을 올리는 경우나, 파일을 올리고 삭제한다음 다시 올리는 경우가 있습니다.

하지만 기본적으로 같은 파일을 다시 업로드 할 경우에는 이벤트가 트리거 되지 않습니다.

그렇기 때문에 파일을 올려준 이후에 value값을 초기화 시켜 주어야합니다.

 

input type="file" value 초기화 시켜주기

import ...

const App = () => {
    const [image, setImage] = useState("");
    
    const imageUpload = (e: ChangeEvent<HTMLInputElement>) => {
        const { target: { files } } = e;
        
        const (files as FileList);
        
        if (file === undefined) return;
        
        reader.onloadend = () => {
            setImage(String(reader.result)):
            
            e.target.value = "";
        }
        
        reader.readAsDataURL(file);
    }
    
    return (
        <input type="file" accept="image/*" onChange={(e) => imageUpload(e)} />
    );
}

e.target.value = "";로 초기화해주면 동일한 파일을 다시 올려도 문제없이 올라갑니다.

반응형
반응형

image를 추가할때 multiple기능을 사용하여 여러 이미지를 추가할 때, File과 미리보기를 담는 방법입니다.

 

import ...

interface dataProps {
    images: string[],
    imagesFile: File[]
}

const App = () => {

    // 메인 이미지 컨텐츠
    const [data, setData] = useState<dataProps>({
        images: [],
        imagesFile: []
    })
    const contentsImage = async (e: ChangeEvent<HTMLInputElement>) => {
        const { target: { files }, currentTarget } = e;

        let file = (files as FileList);

        if (file === undefined) return;

        let newImageList: File[] = [];
        let newImageListPreview: string[] = [];

        for (let i = 0; i < file.length; i++) {
            try {
                const dataUrl = await readAsData(file[i]);
                newImageListPreview.push(dataUrl);
                newImageList.push(file[i]);
            } catch (err) {
                console.log(err);
            }
        }

        setData({...data, images: [...data.images, ...newImageListPreview], imagesFile: [...data.imagesFile, ...newImageList]})
    }
    const readAsData = (file: File): Promise<string> => new Promise((resolve, reject) => {
        const reader = new FileReader();

        reader.onload = () => {
            resolve(reader.result as string);
        }
        reader.onerror = (error) => {
            reject(error);
        }
        reader.readAsDataURL(file);
    })
    return (
        <>
            <input type="file" accept="image/*" onChange={(e) => contentsImage(e)} multiple />
        </>
    );
}

export default App;

이 후, 내용에 확장자 검사, 용량 검사 등등을 추가시켜 주었습니다.

반응형
반응형

태그를 만드는 도중 onKeyPress는 이제 사용되지 않는다는 말을 보고 어떻게 사용해야 할까 고민하다 사용한 방법입니다.

 

태그를 생성할 때, onKeyDown, onKeyUp을 둘 다 사용해 봤지만 일반적인 방법으로는 연속적인 클릭으로 인해 alert창을 띄워도 바로 사라지는 현상이 있었습니다.

onKeyDown은 살짝 누르는 순간에도 연속적으로 내용이 들어가고, onKeyUp은 누를 때 한번 뗄 때 한번 클릭이 되는 바람에 고민하게 된 내용입니다.

 

1. onBeforeInput

깔끔하게 숫자, 영문, 한글은 입력이 되지만 정작 중요한 엔터키가 먹히지 않았습니다.....

 

2. onKeyDown에 preventDefault(); 추가하기

onKeyDown을 사용하면서 가장 윗줄에 이벤트의 기본 동작을 방지해주는 preventDefault()를 추가해 줬습니다.

 

app.tsx

const tagKeyCode = (e: KeyboardEvent<HTMLInputElement>) => {
    const { code: key } = e;
    
    if (key === "Enter") {
        e.preventDefault();
        
        ...
    }
}

위처럼 Enter키를 눌렀을 때, 기본 동작을 방지한 다음 제가 원하는 기능을 집어넣는 방법으로 진행하였습니다.

 

 

:: 그렇다고 onKeyPress를 사용하지 말아야 하는 것은 아닌 것 같습니다. onKeyPress에 경고문구가 뜨기는 하지만, 단지 경고일 뿐이고 사용할 때 뭔가 오류가 난다거나 실서버에서 문제가 생긴다거나 하는 부분은 없었습니다.

 

반응형
반응형

useState를 사용할 때, Object내부에 오브젝트를 변경하는 방법입니다.

항상 useState를 사용할 때, Object는 사용했었지만 Object 안에 Object를 변경하는 방법이 순간 헷갈렸었기에 작성하는 글입니다.

 

예시

import ...

const App = () => {

    const [user, setUser] = useState({
        nickname: "HelloWorld",
        userData: {
            ability: "댄스",
            description: "안녕하세요..."
        }
    });
    return (
        <>
            <input type="text"
                value={user.nickname}
                onChange={(e: ChangeEvent<HTMLInputElement>) => setUser({ ...user, nickname: e.target.value})}
            />
        </>
    );
}

export default App;

위처럼 user의 닉네임을 바꾸기 위해서는 스프레드를 사용해서 간단하게 바꿀수가 있습니다.

그런데 user안의 userData내부 내용을 바꾸기 위해서는 한번 더 안으로 들어가야 합니다.

 

userData의 ability 변경하기

import ...

const App = () => {

    const [user, setUser] = useState({
        nickname: "HelloWorld",
        userData: {
            ability: "댄스",
            description: "안녕하세요..."
        }
    });
    return (
        <>
            <input type="text"
                value={user.userData.ability}
                onChange={(e: ChangeEvent<HTMLInputElement>) => setUser({ ...user, userData: { ...user.userData, ability: e.target.value}})}
            />
        </>
    );
}

export default App;

위처럼 접근하면 user안의 userData내부 내용을 변경할 수 있습니다.

반응형
반응형

Image is missing required "src" property: 에러에 관한 내용입니다.

이미지를 넣는 과정에서 이런 에러가 나오는 경우가 종종 있습니다.

Image src부분이나 alt부분에 제대로된 값이 들어가기전에 한번 실행되기 때문인것으로 생각됩니다.

 

import ...

const App = ({ user }, { user: UserProps }) => {
    
    return (
        <Image width={9999} height={9999} src={user.profile} alt={userWrap.user.nickname + "님의 배경 프로필"} />
    );
}

위와 같은 내용이 있을 때, user에 제대로된 내용이 들어오기전에 undefined가 들어가서 실행을 한번 해줘서 일어나는 현상이라 생각됩니다.

 

해결방법

import ...

const App = ({ user }, { user: UserProps }) => {
    
    return (
        { user.profile && <Image width={9999} height={9999} src={user.profile} alt={userWrap.user.nickname + "님의 배경 프로필"} /> }
    );
}

다른 방법도 있을지 모르지만, "user.profile에 내용이 담기면 실행해라" 라고 일단은 설정을 해두었습니다. 

반응형
반응형

클라이언트 쪽에서 httpOnly cookie내용을 가져오는 방법입니다.

이번에 클라이언트 부분에서 쿠키값을 가져와야 하는 상황이 생겨서 쿠키값을 가져오려 하는데, httpOnly 때문인지는 몰라도 js-cookie, cookies-next 등등을 사용해 봐도 빈 토큰값만 가져오는 상황이 발생했습니다.

:: getServerSideProps는 사용하지 않았습니다.

 

useEffect 훅을 사용해서 pages/api/ 내부에 credentials.ts를 생성하여 서버에서 쿠키내용을 가져오게끔 만들었습니다.

 

pages/api/credentials.ts

import { NextApiRequest, NextApiResponse } from "next";
import cookie from "cookie";

export default function handler(req: NextApiRequest, res: NextApiResponse) {

    const cookies = cookie.parse(req.headers.cookie || "");
    const cookieValue = cookies["cookie-name"];

    res.status(200).json({ cookieValue });
}

 

app.tsx

import ...

const App = () => {

    useEffect(() => {
        const fetchCookieValue = async () => {
            const response = await fetch("/api/credentials", { credentials: "include" });
            const data = await response.json();

            if (data.cookieValue === undefined) {
                // 토큰이 아예 없는 경우
                Swal.fire({
                    title: '로그인',
                    text: "로그인 하러 가시겠습니까?",
                    icon: 'warning',
                    showCancelButton: true,
                    confirmButtonColor: '#3085d6',
                    cancelButtonColor: '#d33',
                    confirmButtonText: '로그인 하러가기',
                    cancelButtonText: "취소"
                })
                .then((result) => {
                    if (result.isConfirmed) router.push("/login");
                    else router.push("/");
                })
            } else {

                await setTokenCookie(data.cookieValue);

                const token: any | null = jwt.decode(data.cookieValue, { complete: true });

                dispatch(userList(token.payload.user[0]));

            }

        };

        fetchCookieValue();
    }, []);
    
    
    return (...);
}
반응형
반응형

달력 만들기

프로젝트에 달력을 추가하기 위해 사용했던 내용입니다.

 

calender.tsx

const CalenderList = () => {
    // 캘린더
    const [calender, setCalender] = useState<string>("");
    // 년 | 달 변경
    const [currentYear, setCurrentYear] = useState(new Date().getFullYear());
    const [currentMonth, setCurrentMonth] = useState(new Date().getMonth() + 1);
    
    // 윤달 체크하기
    const checkLeapYear = (year: number) => {
        if (year % 400 === 0) return true;
        else if (year % 100 === 0) return false;
        else if (year % 4 === 0) return true;
        else return false;
    }
    
    // 각 달의 01일 위치 정해주기
    const getFirstDayOfWeek = (year: number, month: number) => {
        let zero = "";

        if (month < 10) zero = "0";

        return (new Date(year + "-" + zero + month + "-" + "01")).getDay();
    }
    
    // 월 변경하기
    const changeYearMonth = (year: number, month: number) => {
        let monthDay = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

        // 윤달이면 29일 내보내기
        if (month === 2) if (checkLeapYear(year)) monthDay[1] = 29;

        // 01일 위치
        let firstDay = getFirstDayOfWeek(year, month);

        // 이전 달 날짜
        let lastDay = monthDay[month - 1]

        let arrCalender = [];

        // 01일이 생성되기 전 비어있는 내용
        for (let i = 0; i < firstDay; i++) {
            arrCalender.push("");
        }

        // 날짜 넣어주기
        for (let i = 1; i <= monthDay[month - 1]; i++) {
            arrCalender.push(String(i));
        }

        // 마지막 날짜까지 넣고 비어있는 내용
        let remainDay = 7 - (arrCalender.length % 7);

        if (remainDay < 7) {
            for (let i = 0; i < remainDay; i++) {
                arrCalender.push("");
            }
        }

        renderCalender(arrCalender);
    }
    
    // 캘린더 만들기
    const renderCalender = (calender: string[]) => {
        let contents = [];

        for (let i = 0; i < calender.length; i++) {
            if (i === 0) contents.push("<tr>");
            else if (i % 7 === 0) {
                contents.push("</tr>");
                contents.push("<tr>");
            }
            // contents.push("<td>" + "<span>" + calender[i] + "</span>" + `${calender[i] !== "" ? '<Image src={Bob} alt="제목" />' : ""}` + "</td>");
            contents.push(
                `<td>
                    <div class="table_hover">
                        <span class="">${calender[i]}</span>
                        
                        ${calender[i] !== "" ? '<a href="/view/art/art"><Image src="/_next/static/media/1.672483f5.jpg" alt="제목" /></a>' : ""}
                        
                    </div>
                </td>`
            );
        }

        contents.push("</tr>");

        setCalender(contents.join(""));
    }
    
    // 화살표를 클릭했을 때 ( 왼쪽 | 오른쪽 )
    const changeMonth = (diff: number) => {
        setCurrentMonth(prev => prev + diff);
    }
    
    // 이전 달 | 다음 달
    const calenderMemo = useMemo(() => {

        // 1월 아래로 떨어질 때, 12월 위로 올라갈 때
        if (currentMonth < 1) {
            setCurrentYear(prev => prev - 1);
            setCurrentMonth(12);
        } else if (currentMonth > 12) {
            setCurrentYear(prev => prev + 1);
            setCurrentMonth(1);
        }
        changeYearMonth(currentYear, currentMonth);
    }, [currentYear, currentMonth]);
    
    return (
        <div className="calender_wrap">
            <div className="calender__">
                <div className="calender_button_wrawp">
                    <button onClick={() => changeMonth(-1)} className="calender_button_left"><Left /></button>
                    <input type="number" value={currentYear} onChange={(e: ChangeEvent<HTMLInputElement>) => setCurrentYear(parseInt(e.target.value))} />
                    <select value={currentMonth} onChange={(e: ChangeEvent<HTMLSelectElement>) => setCurrentMonth(parseInt(e.target.value))}>
                        <option value="1">1월</option>
                        <option value="2">2월</option>
                        <option value="3">3월</option>
                        <option value="4">4월</option>
                        <option value="5">5월</option>
                        <option value="6">6월</option>
                        <option value="7">7월</option>
                        <option value="8">8월</option>
                        <option value="9">9월</option>
                        <option value="10">10월</option>
                        <option value="11">11월</option>
                        <option value="12">12월</option>
                    </select>
                    <button onClick={() => changeMonth(1)}><Right /></button>
                </div>
                <div style={{border: "1px solid", borderRadius: "8px", overflow: "hidden"}}>
                    <table className="table_border calender_table table_bordered">
                        <thead>
                            <tr>
                                <th>일</th>
                                <th>월</th>
                                <th>화</th>
                                <th>수</th>
                                <th>목</th>
                                <th>금</th>
                                <th>토</th>
                            </tr>
                        </thead>
                        { calender.length > 0 && <tbody dangerouslySetInnerHTML={{__html: calender}}></tbody> }
                    </table>
                </div>
            </div>
        </div>
    );
}

 

원래는 JSX로 아래처럼 표현하려 했지만.. HTML이 아니기 때문에 HTML태그를 따로따로 사용할 수 없다고 해서 위처럼 적용시켰습니다.

const [calenderArray, setCalenderArray] = useState<string[]>([])

// 월 변경하기
const changeYearMonth = (year: number, month: number) => {
    let monthDay = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    // 윤달이면 29일 내보내기
    if (month === 2) if (checkLeapYear(year)) monthDay[1] = 29;

    // 01일 위치
    let firstDay = getFirstDayOfWeek(year, month);

    // 이전 달 날짜
    let lastDay = monthDay[month - 1]

    let arrCalender = [];

    // 01일이 생성되기 전 비어있는 내용
    for (let i = 0; i < firstDay; i++) {
        setCalenderArray((old: string[]) => [...old, ""])
    }
    arrCalender.reverse();

    // 날짜 넣어주기
    for (let i = 1; i <= monthDay[month - 1]; i++) {
        setCalenderArray((old: string[]) => [...old, String(i)])
    }

    // 마지막 날짜까지 넣고 비어있는 내용
    let remainDay = 7 - (arrCalender.length % 7);

    if (remainDay < 7) {
        for (let i = 0; i < remainDay; i++) {
            setCalenderArray((old: string[]) => [...old, ""])
        }
    }

    renderCalender(arrCalender);
}

const calenderRendering = () => {
    const arr = [];
    const result = [];

    for (let i = 0; i < calenderArray.length; i++) {
        if (i === 0) arr.push(i);
        else if (i % 7 === 0) arr.push(i)
    }

    for (let i = 0; i < calenderArray.length; i++) {
        // 안되는부분..
        if (i === 0) result.push("<tr>");
        else if (i % 7 === 0) {
            result.push("</tr>");
            result.push("<tr>");
        }

        result.push(
            <td>
                <div className="table_hover">
                    <span className={`${calenderArray[i] !== "" && "main_background"}`}>{calenderArray[i]}</span>
                    {calender[i] !== "" && <Link href={"/view/art/art"}><Image src={Bob} alt="제목" /></Link>}
                </div>
            </td>
        )
    }

    result.push("</tr>")

    return result;
}

CSS는 뺏습니다 !

반응형
반응형

npm

$ npm install react-draft-wysiwyg draft-js
$ npm install @types/react-draft-wysiwyg

 

editor.tsx

import React, { useState } from 'react';
// import { Editor as Editors } from 'react-draft-wysiwyg';
import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css';
import { EditorState } from 'draft-js';
import styled from 'styled-components';
import dynamic from 'next/dynamic';

const EditorBlock = styled.div`
    .editor_wrap {
        width: 100%;
        margin-bottom: 4rem;
        border: 1px solid #DDD !important;
        border-radius: 8px !important;
    }
    .editor {
        height: 450px !important;
        padding: .5rem !important;
    }
`;

const Editors = dynamic(() => import('react-draft-wysiwyg').then(mod => mod.Editor), {ssr: false});

const Editor = () => {


    // useState로 상태관리
    const [editorState, setEditorState] = useState(EditorState.createEmpty());

    const onEditorStateChange = (editorState: EditorState) => {
        // editorState에 값 설정
        setEditorState(editorState);
    };

    // toolbar
    const toolbar = {
        options: ['inline', 'blockType', 'fontSize', 'fontFamily'],
        inline: {
            options: ['bold', 'italic', 'underline', 'strikethrough'],
        },
        blockType: {
            inDropdown: true,
            options: ['Normal', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Blockquote'],
            className: 'demo-option-custom-wide',
            dropdownClassName: 'demo-dropdown-custom'
        },
        fontSize: {
            options: [8, 9, 10, 11, 12, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96],
            className: "toolbar-class",
            dropdownClassName: 'demo-dropdown-custom',
        },
        fontFamily: {
            options: ['Arial', 'Georgia', 'Impact', 'Tahoma', 'Times New Roman', 'Verdana'],
            className: "toolbar-class",
            dropdownClassName: 'demo-dropdown-custom',
        }
    }

    return (
        <EditorBlock>
            <Editors
                // 에디터와 툴바에 적용
                wrapperClassName="editor_wrap"
                // 에디터 주변에 적용
                editorClassName="editor"
                toolbarClassName="toolbar-class"
                localization={{ locale: "ko" }}
                toolbar={toolbar}
                editorState={editorState}
                onEditorStateChange={onEditorStateChange}
            />
        </EditorBlock>
    );
}

export default Editor;

 

index.tsx

import dynamic from 'next/dynamic';

const Editor = dynamic(() => import("@/components/editor/editor"), {ssr: false});

const Page = () => {
    return (
        <Editor />
    );
}

export default Page;

NextJS에서 Draft.js를 사용하는 거 자체는 어려움이 없었습니다. 

 

문제점

Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the r component.

페이지에 어떤 부위든 클릭을 하면 위와같은 경고문구가 나왔습니다.

처음에는 무조건 ssr때문이라 생각해서 dynamic을 적용시켰지만 전혀 해결될 기미가 보이지 않았습니다.

 

여기저기 둘러보다.... 유일한 문제 해결 방법이 엄격모드 해제라는 내용을 발견하게 되었습니다 ㅠ

엄격모드를 해제해서 고친다는 것은 사실 말이 안 되기 때문에 이 문제를 해결할 방법은 현재 존재하지 않는 듯싶습니다..

2020년부터 많은 사람들이 이 문제 때문에 글을 올렸던 것 같은데 모두 해결했다는 내용은 없었습니다.

결국 SSR 관련 문제이기 때문에 React에서는 문제없이 돌아갈 거라 생각됩니다.

 

결론.. 포기하고 다른 에디터를 찾아봐야겠습니다..!!

 

Warning관련 github 바로가기

 

반응형
반응형

npm

$ npm install --save react-editor-js @editorjs/editorjs
$ npm install --save @editorjs/header
$ npm install --save @editorjs/list
$ npm install --save @editorjs/code
$ npm install --save @editorjs/paragraph
$ npm install --save @editorjs/checklist

 

editorTools.js

editor를 정의해줄 내용이 없기 때문에 editorTools부분은 js로 작성했습니다.

// import Code from "@editorjs/code";
import Header from "@editorjs/header";
import Paragraph from "@editorjs/paragraph";
import List from "@editorjs/list";
// import CheckList from "@editorjs/checklist";

export const EDITOR_TOOLS = {
    header: {
        class: Header,
        inlineToolbar: true,
        shortcut: "CMD+SHIFT+H"
    },
    paragraph: { class: Paragraph, inlineToolbar: true },
    list: {
        class: List,
        inlineToolbar: true
    }
};

 

tsconfig.json

js를 사용했기 때문에 아래처럼 include부분에 js를 사용했다고 알려주었습니다.

"include": ["src/components/editor/editorTools.jss.js"]

 

editor.tsx

import React, { memo, useEffect, useRef } from "react";
import EditorJS, { OutputData } from "@editorjs/editorjs";
import { EDITOR_TOOLS } from "./EditorTools";

//props
type Props = {
  data?: OutputData;
  onChange(val: OutputData): void;
  holder: string;
};

const EditorBlock = ({ data, onChange, holder }: Props) => {
  // 참조 추가
  const ref = useRef<EditorJS>();

  // 초기화
  useEffect(() => {
    // 참조가 없을때 초기화 합니다.
    if (!ref.current) {
      const editor = new EditorJS({
        holder: holder,
        tools: EDITOR_TOOLS,
        data,
        async onChange(api, event) {
          const data = await api.saver.save();
          onChange(data);
        },
      });
      ref.current = editor;
    }

    return () => {
      if (ref.current && ref.current.destroy) {
        ref.current.destroy();
      }
    };
  }, []);


  return <div id={holder} />;
};

export default memo(EditorBlock);

 

index.tsx

import dynamic from "next/dynamic";
import { OutputData } from '@editorjs/editorjs';

const EditorBlock = dynamic(() => import("@/components/editor/editor"), { ssr: false });

const Page = () => {
    
    const [data, setData] = useState<OutputData>();
    
    return (
        <EditorBlock data={data} onChange={setData} holder="editorjs-container" />
    );
}

export default Page;

 

반응형
반응형

[...id].tsx 컴포넌트에서 router.query 안에 object를 사용할때 "Object is possibly 'undefined'" 오류로 인해 작성하게 된 내용입니다.

router.asPath로 주소를 모두 가져올수도 있지만, 모든 내용을 가져오고 싶지 않아서 찾아보게 된 내용입니다.

 

router.isReady

isReady는 라우터 필드가 클라이언트 측에서 업데이트되고 사용할 준비가 되었는지 여부입니다.

useEffect 내부에서만 사용해야 하며 서버의 조건부 렌더링에는 사용할 수 없습니다.

 

그런데 아래처럼 들어오는걸 원하지는 않았습니다. 그것도 Object로...

const { query: { id }, isReady } = useRouter();

useEffect(() => {
    if (!isReady) return;
    console.log(id); // ['home'];
}, [id])

 

아래와 같은 방식으로 query내부에 내용을 가져왔습니다.

const router = useRouter();

useEffect(() => {
    if (router.query["id"] !== undefined) console.log(router.query["id"][0]); // home
}, [])
반응형

+ Recent posts