The centralized storage method more or less lines up with what I have recommended to others in the past for holding similar user specific information, like personalization and such. You could expand it to hold additional data other than just column visibility. (Sorting preferences or similar). Just make sure the top level property is set to private.
One pitfall I could see is if a single table is used for different queries, you could run into a case where column A needs to be hidden for query A, but should be shown for query B. That means you might need to include additional identifier information in your config data to indicate if the column visibility is for the table in general or a specific query on that table.
Writing directly to the session props could leave you with orphaned key/value pairs if you change what columns are available for the table after saving a config, but that's a pretty negligible cost unless you have thousands of orphaned items. You could also run a cleanup operation on the storage table if you really needed/wanted to clean out stale keys.
Are you able to modify your existing function that generates your columns to take an optional userConfig argument and pass the config for the table through that?
Then when you are doing whatever logic to loop through and create the columns, you can check immediately to see if the column should be hidden or not. This would save you from looping over the list of columns again after their generation.
I would still keep the helper for any post generation changes. On submission of new config, pass the existing generated columns, new config, and table Id, and adjust the columns based on the new config.