리액트를 다루는 기술 25장을 공부하다가 quill 적용이 잘 안되는 것을 확인했습니다.
1. 문제 발견
quill에서 theme를 정해주는 부분에서 bubble을 적용할 경우,
웹에 quill 관련 ui가 나오기는 하는데, placeholder의 값이 바로 나오지 않고 수많은 엔터가 쳐져있음.
말이 주저리주저리라 사진으로 표현해보겠습니다..
이 상황에서 저 공백들을 지우면 placeholder 값이 나오기는 하지만 새로고침을 하면 계속 저런 공백이 생깁니다.
2. 해결 방법 도전
useEffect(() => {
quillInstance.current = new Quill(quillElement.current, {
theme: 'bubble',
placeholder: '내용을 작성하세요...',
modules: {
// 더 많은 옵션
// https://quilljs.com/docs/modules/toolbar/ 참고
toolbar: [
[{ header: '1' }, { header: '2' }],
['bold', 'italic', 'underline', 'strike'],
[{ list: 'ordered' }, { list: 'bullet' }],
['blockquote', 'code-block', 'link', 'image'],
],
},
});
quill 관련해서 에러가 있다고 판단해서 app.js -> index.js -> writepage.js -> editorContainer.js -> editor.js 등 으로 접근해 editor.js, write.js 에서 여러번 console.log을 찍었습니다. 그런데 딱히 문제를 찾을 수 없었습니다..
redux에서도 값이 잘 들어가고..
콘솔을 아무리 찍어도 실제로 보여지는 에러가 없었기 때문에.. (f12를 눌러도 아무런 에러가 없었음..ㅠ)
절대 안 하려 했지만 .. 3방법으로 도전했습니다..(에러는 해결해야하니까ㅠㅠ)
3. 해결
요약
1. 리액트 버전 18 -> 17 변경(package.json)
2. index.js 수정
리액트를 다루는 기술의 github에 들어가서 대체 뭐가 다르길래 이게 안되는지 확인하러 갔습니다.
https://github.com/gilbutITbook/080203/tree/master/25/blog/blog-frontend
제 코드를 위 코드와 바꿔가면서 적용해보았습니다.
container, component, pages, lib, modules, App 에는 문제(다른 점)가 전혀 없었습니다..
마지막으로 설마???? 하는 마음으로 바꾼 것이 package.js와 index.js 입니다.
package.json에서 react 버전을 바꾼 후 index.js의 내용도 교체했습니다. 책의 버전은 리액트 17인데 제 버전은 리액트 18이기에 이런 오류가 났다고 판단했습니다..
1. 리액트 버전 18 -> 17
(구) package.json
{
"name": "blog-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.19.0",
"immer": "^9.0.15",
"quill": "^1.3.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^8.0.4",
"react-router-dom": "^6.4.1",
"react-scripts": "5.0.1",
"redux": "^4.2.0",
"redux-actions": "^2.6.5",
"redux-devtools-extension": "^2.13.9",
"redux-saga": "^1.2.1",
"styled-components": "^5.3.6",
"web-vitals": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:4000/"
}
(신) package.json
{
"name": "blog-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.19.0",
"immer": "^3.1.3",
"quill": "^1.3.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-redux": "^7.1.0",
"react-router-dom": "^6.2.1",
"react-scripts": "^5.0.0",
"redux": "^4.0.1",
"redux-actions": "^2.6.5",
"redux-devtools-extension": "^2.13.8",
"redux-saga": "^1.0.3",
"styled-components": "^4.3.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://localhost:4000/",
"devDependencies": {
"web-vitals": "^3.0.4"
}
}
2. index.js 수정
리액트 버전이 바뀐 만큼 src/index.js를 수정해주어야 했습니다.
중요한 점은 리액트 18에는 serviceWorker.js 이 없고 리액트 17에는 reportWebVitals.js 이 없습니다.reportWebVitals.js의 자리를 serviceWorker.js로 바꿔줍니다.
(구) index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { createStore, applyMiddleware } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import createSagaMiddleware from 'redux-saga'
import rootReducer, {rootSaga} from './modules'
import { tempSetUser, check } from './modules/user'
const root = ReactDOM.createRoot(document.getElementById('root'))
const sagaMiddleware = createSagaMiddleware()
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(sagaMiddleware)))
function loadUser() {
try {
const user = localStorage.getItem('user')
if (!user) return; // 로그인 상태가 아니라면 아무것도 안 함
store.dispatch(tempSetUser(JSON.parse(user)))
store.dispatch(check())
} catch (e) {
console.log('localStorage is not working')
}
}
// sagaMiddleware을 실행한 후 loadUser을 실행해야 함! -> loadUser를 먼저 호출하면 CHECK 액션을 디스패치했을 때 사가에서 제대로 처리하지 않음
sagaMiddleware.run(rootSaga)
loadUser()
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>,
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()
(신) index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import createSagaMiddleware from 'redux-saga';
import rootReducer, { rootSaga } from './modules';
import { tempSetUser, check } from './modules/user';
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware)),
);
function loadUser() {
try {
const user = localStorage.getItem('user');
if (!user) return; // 로그인 상태가 아니라면 아무것도 안함
store.dispatch(tempSetUser(user));
store.dispatch(check());
} catch (e) {
console.log('localStorage is not working');
}
}
sagaMiddleware.run(rootSaga);
loadUser();
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
serviceWorker.unregister();
+ 참고한 사이트
Uncaught TypeError: react_dom__WEBPACK_IMPORTED_MODULE_1__.createRoot is not a function at ./src/index.js
https://stackoverflow.com/questions/46566830/how-to-use-create-react-app-with-an-older-react-version
+ src/serviceWorker.js
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
4. 기타 및 의문점
에러는 해결 되었는데, 그럼 리액트 18에서는 quill을 제대로 적용할 수 있는 방법이 없나..
그리고 이 방법으로 해결해도 snow일 때는 해결이 되지 않는다는 점..ㅠ