이것저것 만들어 보는 곳 특히, UI 관련 작은 파츠들
dropdown
gitlab의 repository 설정하는 부분의 드롭다운을 참고하였음
slider
- made slider with react-slick
- what is hammer
- what is swipeableview
form
- 이미지 업로드
- label만 보여주고 나머지 input 부분을 숨길 수 도 있구나
- formData가 뭔지 알아보기
- 이미지가 포함되면 서버에선 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryjHlBOsNKdipheLFR' 으로 전달되고
- body엔 값이 없음
loading
- innerHTML로 하거나
- 로딩을 넣어놓고 style.display = 'block' or 'none' 으로도 가능
- loading by style
- loading by innerHTML
speech bubble
- :after와 border-color: transparent transparent transparent #ad1e51; 로 구현
- speech bubble
pagination
validation
// html validation
<input
type="tel"
id="phone"
name="phone"
placeholder="123-45-678"
pattern="[0-9]{3}-[0-9]{2}-[0-9]{3}"
/>
image resize
글자수 제한
<textarea
rows="10"
class="form-control"
id="textArea_byteLimit"
name="textArea_byteLimit"
onkeyup="fn_checkByte(this)"
></textarea>
// fn_checkByte(obj) => console.log(obj) : <textarea> element
대부분 코드가 다 똑같다.
regex
특수문자 제한
drag and drop
<div draggable="true">draggable</div>
- draggable="true"로 지정하면 해당 element를 드래그하면 따라오는 그림이 생긴다
To trigger an action on drag or drop on DOM elements, we’ll need to utilize the Drag and Drop API:
- ondragstart: This event handler will be attached to our draggable element and fire when a dragstart event occurs.
- ondragover: This event handler will be attached to our dropzone element and fire when a dragover event occurs.
- ondrop: This event handler will also be attached to our dropzone element and fire when a drop event occurs.
<div
id="draggable-1"
class="example-draggable"
draggable="true"
ondragstart="onDragStart(event)"
>
draggable
</div>
- dropzone을 설정 시 원하는 박스에 넣으려면 closest로 타겟을 변경해줘야 함
const dropzone = event.target.closest(".example-dropzone");
그렇지 않으면 child에 append가 되어버림
- dragstart : 클릭을 꾹 누르고 있으면 dragstart 이벤트가 발생함
- dragover : drag를 할 수 있다는 표시가 생김
- dragenter : 해당 영역에 들어오면 발생
- dragleave : 해당 영역을 벗어나면 발생
- 화면의 x, y에(relative to the viewport) 위치한 element를 return
let insertedNode = parentNode.insertBefore(newNode, referenceNode);
- The Node.insertBefore() method inserts a node before a reference node as a child of a specified parent node.
-
event.clientY : event.target안에서 x, y 값
-
element.getBoundingClientRect() : 화면안에서 엘리먼트가 위치한 곳의 좌표
- https://codepen.io/fitri/pen/VbrZQm
- https://developer.mozilla.org/en-US/docs/Web/API/Document/elementFromPoint
- https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore
- swap animation
- https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect
- drag event 발생한 객체의 위치를 기억
- 시작점과 현재 위치의 차이만큼 시작점에 더한 값이 이동해야할 위치
- 방법 1
- 두 거리를 빼서 그 만큼 이동
- 예상 문제점 이동 중 또 이벤트가 발생되면서 중간으로 수렴
- 두 거리를 빼서 그 만큼 이동
- 방법 2
- 고정된 거리만큼 이동
- 예상 문제점 이동 중 또 이벤트가 발생되면서 중간으로 수렴
- 단, 위와 아래 방향을 잘 찾을 수 있어야 함
- 고정된 거리만큼 이동
- 방법 1
- 두 거리를 뺀 거리만큼 하되, onDragEnter에서 이동이 이루어 지는데 이동 중간에 다시 onDragEnter이벤트가 발생되면서 중간에 수렴하거나 점점 멀어짐
- transition start와 end에서 flag를 만들어서 애니메이션 중간엔 onDragEnter 함수가 실행되지 않도록 함
- 우려되는 점 : 빠르게 스크롤을 내릴 경우 비정상 적으로 작동할 가능성이 있음
- transition start와 end에서 flag를 만들어서 애니메이션 중간엔 onDragEnter 함수가 실행되지 않도록 함
- 드래그 중인 요소가 다른 요소를 만났을 때 그 위치로 원본이 이동
- 드롭의 순간에서 최종적으로 DOM을 교체하고 애니메이션이 적용된 모슨 요소들의 스타일을 제거
- DOM 교체를 Node.insertBefore()를 사용할 건지 아닌지 생각해봐야 함
<li
class="item"
draggable="true"
ondragstart="onDragStart(event)"
ondragover="onDragOver(event)"
>
Draggable Element One
</li>
- onDragStart의 이름을 ondragstart로 하면 자기 자신을 실행하면서 Maximum call stack size exceeded 에러가 발생한다.
- 큐에 넣고 애니메이션 시간 동안 같은 객체라면 다시 이벤트가 발생되지 않도록 해보면 어떨까
- 자식 노드가 포함된 NodeList를 반환
- HTMLElement뿐만 아니라 텍스트 등도 포함
- 현재 요소의 자식 요소가 포함된 HTMLCollection을 반환
- 비 요소 노드는 모두 제외 됨
- https://zetawiki.com/wiki/%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8_%EB%B0%B0%EC%97%B4_%EC%88%9C%EC%84%9C_%EB%B0%94%EA%BE%B8%EA%B8%B0
- temp를 하나 만들어서 둘의 값을 바꿔줌
- 순서가 빠르게 순간적으로 여러번 바뀌는 현상
- https://github.com/woowa-techcamp-2020/todo-14/blob/main/doc/Drag-and-Drop-with-Animation.md
- Element.children vs Node.childNodes
- 선택하면 on_chosen 클래스가 li에 붙는데 css를 위한 것은 아닌 것 같다
- 라이브러리 쓰니까 되게 쉽게 되는 것 같아 보이네
- https://www.codingnepalweb.com/drag-drop-list-or-draggable-list-javascript/
- https://cdnjs.com/libraries/Sortable
- 유튜브 영상 참고해서 만들기
function positionItems() {
let itemsList = document.querySelectorAll(".items .item");
let indexCounter = 0;
itemsList.forEach((item) => {
item.style.top = 70 * indexCounter + indexCounter * 10 + "px";
// 기본적으로 위치는 동일하지만(absolute) 각각 자바스크립트로 위치를 조정함
indexCounter++;
});
}
- parentNode로 부터 떨어진 거리
- 만약 parentNode가 없다면 body(0, 0)에서 떨어진 거리
- 현재 화면(스크롤에 상관없이 상대적인)의 마우스 좌표의 Y값
- 코드 완전 분석
- 현재 마우스 포인터가 절반 이상 넘어가야 이동되는데 진입하자마자 이동되도록 변경해야 함
-
draggable을 true로 주면 mouse up, mouse move가 동작을 안함
- 우선 지금 따라한 것을 완전히 이해한 다음 draggable을 사용했을 때 구현방법을 생각해보자
-
Array.prototype.slice()
-
순서 정렬 : order에 따라 정렬하기 때문에 order를 사용하지 않는다면 쓸 수 없음
-
애니메이션 시간 동안 resetTransition true, false 설정하는 것 : 지속 시간 동안 다시 줍는 것 막음
- project name에 대문자가 포함될 수 없음
import React, { useState } from "react";
import { ReactSortable } from "react-sortablejs";
const BasicFunction = () => {
const [state, setState] = useState([
{ id: 1, name: "shrek1" },
{ id: 2, name: "fiona2" },
{ id: 3, name: "shrek3" },
{ id: 4, name: "fiona4" },
{ id: 5, name: "shrek5" },
{ id: 6, name: "fiona6" },
]);
return (
<ReactSortable
list={state}
setList={setState}
group="groupName"
animation={200}
delay={2}
>
{state.map((item) => (
<div key={item.id}>{item.name}</div>
))}
</ReactSortable>
);
};
export default BasicFunction;
download
<!-- 저장하려는 파일이 동일URL인 경우만 가능 -->
<a href="apple.png" download="새로운이름(사과)"></a>
- download 속성 없이도 a 태그에 href에 다운로드 URL을 넣어주면 다운로드가 된다
- download 속성 : 파일이 서버안에 있어야 다운로드가 되는 것 같다.
- download only works for same-origin URLs, or the blob: and data: schemes.
- 브라우저는
<a>
태그에 download 속성이 설정되어 있으면 링크가 가리키는 파일을 다운로드한다. 즉, 마치 링크 위에서 마우스 오른쪽 버튼을 클릭하고 "다른 이름으로 링크 저장"을 실행하는 것과 같다. - img의 경우 same-origin URL이 아니더라도 img를 a태그로 감싸면 download에 부여한 새로운 이름으로 저장할 수 있다.
downloadFile() {
const blob = new Blob([this.content], {type: 'text/plain'})
const url = window.URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `${this.$store.state.nickname}_${this.title}.md`
a.click()
a.remove()
window.URL.revokeObjectURL(url);
},
- Binary Large Object : 바이너리 데이터를 저장할 수 있는 데이터 유형
function download() {
axios({
url: "https://source.unsplash.com/random/500x500",
// url: 'https://wetubetony.s3.ap-northeast-2.amazonaws.com/video/6a3261c1aae8da977fb6a4fc51dcc116', // CORS
method: "GET",
responseType: "blob",
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
link.href = url;
link.setAttribute("download", "image.jpg");
// link.setAttribute('download', 'video.mp4');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
}
WYSIWYG
- What you see is what you get
- HTML 에디터
- 웹에선 게시글 등을 작성할 때 HTML를 직접 작성하지 않아도 글씨 크기를 수정할 수 있고 이미지 등을 업로드하는 것을 도와주는 에디터
- 서버는 inflearn-clone-back의 test.ts
- 이미지 업로드는 base64 형태로 인코딩이 되어서 전송됨
<!-- 전송 샘플 -->
<p>테스트123</p>
<p><br /></p>
<p>테스트456</p>
<p><br /></p>
<p><b>테스트</b></p>
<p>
<b><br /></b>
</p>
<p>
<font style="background-color: rgb(255, 255, 0);" color="#ff0000"
>하하호호</font
>
</p>
<p><br /></p>
<p>
<!-- 이미지도 전송 가능 -->
<img
style="width: 833px;"
src="data:image/jpeg;base64,/9j/4QFgRXhpZgAATU0AKgAAAAgABwEAAAMAAAABDMAAAAEQAAIAAAAJAAAAYgEBAAMAAAABCZAAAAEPAAIAAAAIAAAAa4dpAAQAAAABAAAAhwESAAMAAAABAAgAAAEyAAI ...
e1+kMITpo/qYPtvbWIt/YweujW6awW//9k="
data-filename="20200211_162422.jpg"
/>
<br />
</p>
PayloadTooLargeError: request entity too large
at readStream (C:\github\inflearn-clone-back\node_modules\raw-body\index.js:155:17)
at getRawBody (C:\github\inflearn-clone-back\node_modules\raw-body\index.js:108:12)
at read (C:\github\inflearn-clone-back\node_modules\body-parser\lib\read.js:77:3)
at urlencodedParser (C:\github\inflearn-clone-back\node_modules\body-parser\lib\types\urlencoded.js:116:5)
at Layer.handle [as handle_request] (C:\github\inflearn-clone-back\node_modules\express\lib\router\layer.js:95:5)
at trim_prefix (C:\github\inflearn-clone-back\node_modules\express\lib\router\index.js:317:13)
at C:\github\inflearn-clone-back\node_modules\express\lib\router\index.js:284:7
at Function.process_params (C:\github\inflearn-clone-back\node_modules\express\lib\router\index.js:335:12)
at next (C:\github\inflearn-clone-back\node_modules\express\lib\router\index.js:275:10)
at jsonParser (C:\github\inflearn-clone-back\node_modules\body-parser\lib\types\json.js:119:7)
- 미리 작성된 양식을 에디터에 넣어서 보여줘야되는데 이건 어떻게 해야하지?
- 업로드를 바로 시켜서 URL로 주고 받아야 되나?
-
https://velog.io/@gth1123/tinyMCE-WYSIWYG-%EC%82%AC%EC%9A%A9
-
npx create-react-app tinymce-react-demo -template typescript
-
cd tinymce-react-demo
-
npm install --save @tinymce/tinymce-react
rerendering, component separation
- npx create-react-app react_render_practice
javascript
- globalNumber라는 값이 변화될 것으로 예상했으나 그렇지 않았다.
- 함수의 parameter로 변수를 받아서 그것의 값을 변경해도 전달한 원래의 변수는 값이 변화되지 않는다.
- 그런데 신기한점은 함수내에서 변수를 선언하지 않았고 단지 전달만 받은 변수가 계속 살아서 값이 누적될 수 있다.
Recoil
- Recoil 프로젝트 clone해서 코드 살펴보기
- Recoil로 어떻게 global state를 선언하는지
- 선언한 global state를 컴포넌트 안에서 사용하는 방법
- Recoil 관련해선 pages/Landing.tsx만 살펴봄
- Difficulty(select 태그)에 들어가는 option들을 global state로 만들음
- 이런 옵션들을 다른 페이지에서도 사용해야 하기 때문
- DB에 저장해서 관리하는게 편하지 않나?
- db에서 난이도에 대한 string[] 를 가져오고
- 첫 페이지에서 선택한 난이도를 앱 전체에서 사용하기 위함
- state directory안에 모여 있음
// src/state/QuizDifficulty.ts
import { atom } from "recoil";
export default atom<string | undefined>({
key: "QuizDifficulty",
default: undefined,
}); // 앱 전체에서 사용될 첫 페이지에서 선택한 난이도
- recoil 라이브러리에서 atom 이란 함수를 가져옴
- atom
- 객체를 파라미터로 받는 함수
- key, default를 가지고 있는 객체
- key : 유니크한 값이 들어가야 함
- atom으로 만들어낼 global state에 대해 모두 각각 유니크한 key를 가지고 있어야 함
- default : 우리가 선언한 global state에 할당하고 싶은 default값
- key : 유니크한 값이 들어가야 함
- atom
// src/components/Organisms/QuizDifficulty.tsx
import { useRecoilState } from "recoil";
import { QuizDifficultyState } from "src/state";
const QuizDifficulty = () => {
const [quizDifficulty, setQuizDifficulty] =
useRecoilState(QuizDifficultyState);
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
setQuizDifficulty(e.target.value);
};
return (
<select
data-testid={DIFFICULTY_SELECT_TEST_ID}
margin="16px 0px"
value={quizDifficulty}
onChange={handleChange}
>
{difficulties.map((difficulty) => (
<option
key={difficulty}
value={difficulty == ANY_DIFFICULTY ? undefined : difficulty}
>
{difficulty == ANY_DIFFICULTY ? difficulty : difficulty.toUpperCase()}
</option>
))}
</select>
);
};
export default QuizDifficulty;
- recoil의 atom으로 만든 global state를 사용하기 위해서
useRecoilState
라는 hook을 사용- useRecoilState에 atom으로 선언한 것을 전달
- const [quizDifficulty, setQuizDifficulty] =
useRecoilState(
QuizDifficultyState
);
- const [quizDifficulty, setQuizDifficulty] =
useRecoilState(
- useRecoilState에 atom으로 선언한 것을 전달
- useState랑 똑같이 사용하면 됨
- 비동기적인 global state를 사용해서 렌더링할 때 suspense 사용하기
- atom과 같이 global state를 선언하는 함수
-
- 이미 선언된 atom이 값이 변할 때, 그 atom을 구독하고 있다가 selector에 할당된 함수가 다시 실행
-
- 서버와 비동기적으로 통신한 response data를 값으로 가질 수 있음
- clone했기 때문에 작성자의 서버주소가 어딘지 열려있는지 확인이 안됨
// src/components/Organisms/LandingFooter.tsx
import { useResetRecoilState } from "recoil";
import { InitialPropsState } from "src/state";
useResetRecoilState(InitialPropsState);
// InitialPropsState.ts : selector
import { selector } from "recoil";
export default selector<TResponseData>({
// atom이 아닌 selector로 선언된 global state
key: "initialOrderState", // atom포함해서 unique한 key이어야 함
get: async ({ get }) => {
const queryData = get(QueryDataState); // atom으로 선언된 global state를 구독하고 있다가 변경되면 get: 에 할당된 async함수가 재 실행 됨
// QueryDataState가 변경 될 때 마다 서버로 부터 받아온 데이터(decodedResponseData)를 return
if (
queryData == undefined ||
window.location.pathname != `/${QUIZ_PAGENAME}`
)
return undefined;
const { amount, difficulty } = queryData;
const axios = customAxios();
const response = await axios({
method: "GET",
params: {
amount,
difficulty,
type: "multiple",
},
});
const decodedResponseData = {
...response.data,
results: response.data.results.map((quiz: TQuiz) => {
const decoded_correct_answer = decodeHtml(quiz.correct_answer);
const decoded_incorrect_answers = quiz.incorrect_answers.map((answer) =>
decodeHtml(answer)
);
return {
...quiz,
question: decodeHtml(quiz.question),
correct_answer: decoded_correct_answer,
incorrect_answers: decoded_incorrect_answers,
examples: addCorrectAnswerRandomly(
decoded_incorrect_answers,
decoded_correct_answer
),
};
}),
};
return decodedResponseData;
},
set: ({ get, set }) => {
const amount = get(QuizNumbersState); // atom state를 가져와서
const difficulty = get(QuizDifficultyState); // atom state를 가져와서
set(QueryDataState, { amount, difficulty }); // QueryDataState : atom state를 업데이트 해줌 -> get: 에서 QueryDataState를 구독하고 있으므로
// useResetRecoilState()로 set:을 호출해서 set으로 값을 업데이트 하면
// selector의 get: 에 할당된 async 함수가 실행 됨
set(QuizNumbersState, DEFAULT_NUMBERS);
set(QuizDifficultyState, undefined);
},
});
- atom과 같이 global state를 선언하는 함수
-
- 이미 선언된 atom이 값이 변할 때, 그 atom을 구독하고 있다가 selector에 할당된 함수가 다시 실행
-
- 서버와 비동기적으로 통신한 response data를 값으로 가질 수 있음
- get에 할당된 함수에서 서버와 통신을 함
- get에 할당된 함수의 prop : { get } 으로 atom state를 구독하고 있다가 변경되면 할당된 async 함수가 재실행 됨
- 즉, atom state를 구독하고 있다가 변경되면 서버로 부터 데이터를 다시 불러와서 서버로 부터 온 데이터를 return
- set property에 어떤 것도 할당되지 않았다면,
- selector는 자체적으로 setState, atom처럼 setState를 할 수 없음
- selector는 state본체라기 보단 atom의 파편, atom을 무조건 subscribe 해야함
- set은 selector가 어떻게 setState를 하라고 명시해주는 것
- selector의 setState를 하면 set에 할당된 함수가 실행 됨
- selector state의 set에 할당된 함수를 실행
- useResetRecoilState()로 set:을 호출해서 set으로 값을 업데이트 하면
- QueryDataState : atom state를 업데이트 해줌
- get: 에서 QueryDataState를 구독하고 있으므로
- selector의 get: 에 할당된 async 함수가 실행 됨
- tree 처럼 앞의 state가 수정되면 selector도 재실행이 됨
- children으로 호출하는 컴포넌트 중에서 어떤 특정 컴포넌트가 비동기 데이터를 읽어오고 있다면
- 비동기 값의 loading, success, fail 상태 일때
- loading 상태일 땐, Suspense컴포넌트의 fallback(prop)에 해당하는 컴포넌트를 렌더링해줌
- loading이 끝나고 success 또는 fail이면 다시 children 컴포넌트를 렌더링
import { Suspense } from "react";
import { Helmet } from "react-helmet";
import { Route, Switch } from "react-router";
import { BrowserRouter } from "react-router-dom";
import { QUIZ_PAGENAME, RESULT_PAGENAME } from "src/constant";
import {
ErrorBoundary,
LandingPage,
QuizPage,
ResultsPage,
ShimmerPage,
} from "src/components/Pages";
const Router = () => {
return (
<BrowserRouter>
<ErrorBoundary>
<Suspense fallback={<ShimmerPage />}>
<Switch>
<Route path={`/${QUIZ_PAGENAME}`}>
<Helmet title="Quiz page" />
<QuizPage />
</Route>
<Route path={`/${RESULT_PAGENAME}`}>
<Helmet title="Result page" />
<ResultsPage />
</Route>
<Route exact path="/">
<Helmet title="Landing page" />
<LandingPage />
</Route>
</Switch>
</Suspense>
</ErrorBoundary>
</BrowserRouter>
);
};
export default Router;
// https://opentdb.com/api.php?amount=1&difficulty=easy
{
"response_code": 0,
"results": [
{
"category": "Entertainment: Music",
"type": "multiple",
"difficulty": "easy",
"question": "Which Beatles album does NOT feature any of the band members on it's cover?",
"correct_answer": "The Beatles (White Album)",
"incorrect_answers": ["Rubber Soul", "Abbey Road", "Magical Mystery Tour"]
}
]
}
- 테스트 할 때 마다 모듈을 설치해야되는 부분이 번거로우므로 여기에서 nextjs 관련 테스트를 모두 진행할 예정
- https://nextjs.org/docs/getting-started
- npx create-next-app@latest --typescript
context api
context api 좋은지 모르겠다 provider로 감싸야 하는 계층구조가 좀 별로다
react query
{
"fact": "Cats step with both left legs, then both right legs when they walk or run.",
"length": 74
}
// _app.jsx
import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
export default function MyApp({ Component, pageProps }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
);
}
open api
open api
- nextjs-typescript에 구현
- 설명 : https://velog.io/@gth1123/react-infinite-scroll-component
Redux Toolkit TypeScript Example
git 여러 커밋 합치기
https://velog.io/@gth1123/git-%EC%97%AC%EB%9F%AC-%EC%BB%A4%EB%B0%8B-%ED%95%A9%EC%B9%98%EA%B8%B0
Tictactoe
copilot으로 만들어본 Tictactoe 게임
check box
체크박스 비교
- aria-checked
- input : check
- 별거 없다 그냥 aria-checked 안쓰는게 나을 듯
- 자바스크립트로 다 설정해줘야 함
npm i -D typescript
npx tsc --init
- https://git-scm.com/docs/git-revert
- reset과 달리 revert는 커밋로그는 유지하면서 커밋하기 전으로 되돌릴 수 있다